開発情報・ナレッジ

投稿者: 株式会社グローアカルチャ 2025年9月29日 (月)

祝日マスタの自動メンテナンス方法について

内閣府から祝日の一覧を取得+DBへ格納し、祝日マスタをプログラムでメンテナンスする方法をします。

概要

・祝日情報は内閣府(www8.cao.go.jp)から取得する
・取得したデータをDBに格納する
・一定期日前より前の日付は取り込まない
・12/31、1/2、1/3は追加で登録する

祝日マスタの登録について

内閣府から祝日データを自動取得し、SPIRALのデータベースに一括登録するPHPスクリプトを紹介します。

実装の概要

1. 内閣府の祝日CSVデータをcURLで取得する。
2. 取得したCSVデータをパースし、指定日付以降のデータをフィルタリングする。
3. 大晦日や三が日などの追加休日を設定する。
4. 日付順にソートしてデータを整理する。
5. SPIRAL APIを使用してデータベースに一括登録・更新する。
6. 処理結果を表示する。

データベース設計

祝日マスタのデータベース構造は以下の通りです。
項目名 識別名 フィールドタイプ DB上で必須な属性
日付 holidayAt 日付(○年○月○日) 必須制約
ユニーク制約
主キー
休日名 body テキストフィールド(128 bytes) なし
サンプルデータ
日付 休日名
2025/1/1 元日
2025/1/13 成人の日
2025/2/11 建国記念の日

PHPコード(メイン機能)

祝日データを自動取得・登録するPHPスクリプトです。

コピー
<?php

/**
 * 休日登録サービス
 * 内閣府から休日データを取得し、Spiral APIを使用してデータベースに登録する
 */

// ==================== 設定定数 ====================
// 環境ごとに設定してください
const SPIRAL_API_TOKEN = '';
const SPIRAL_API_SECRET = '';

// マスタ登録する日付の下限
const WITHOUT_LOWER_DATE = '2025-01-01';

class HolidayRegisterService
{
    // 内閣府の休日データを取得するURL
    private const SHUKUJITU_CSV_URL = 'https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv';
    
    // データベース設定 
    // データベース名は環境ごとに設定してください
    private const DB_TITLE = 'holiday';
    private const KEY_COLUMN = 'holidayAt';
    private const COLUMNS = ['holidayAt', 'body'];

    /**
     * 値が空でないかチェックする関数
     *
     * @param mixed $value チェックする値
     * @return bool 値が存在し、空でない場合はtrue
     */
    private function filled($value): bool
    {
        if (is_null($value)) {
            return false;
        }

        if (is_string($value)) {
            return trim($value) !== '';
        }

        if (is_array($value)) {
            return !empty($value);
        }

        return true;
    }

    /**
     * cURLを使用してHTTPリクエストを実行する
     *
     * @param string $url リクエストURL
     * @param array $options cURLオプション
     * @return string レスポンス
     * @throws Exception cURLエラーが発生した場合
     */
    private function executeCurlRequest(string $url, array $options = []): string
    {
        $curl = curl_init();
        
        // デフォルトオプションを設定
        $defaultOptions = [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_FOLLOWLOCATION => true,
        ];
        
        // オプションをマージ(追加オプションが優先)
        $mergedOptions = $options + $defaultOptions;
        
        curl_setopt_array($curl, $mergedOptions);
        
        $response = curl_exec($curl);
        
        if (curl_errno($curl)) {
            $error = curl_error($curl);
            curl_close($curl);
            throw new Exception("cURL error occurred: {$error}");
        }
        
        curl_close($curl);
        
        return $response;
    }

    /**
     * 内閣府から休日データを取得する
     *
     * @param DateTime|null $lower 取得する日付の下限(nullの場合は全期間)
     * @return array 休日データの配列 [['日付', '休日名'], ...]
     * @throws Exception データ取得に失敗した場合
     */
    private function fromCabinetOffice(?DateTime $lower = null): array
    {
        // 下限日付を設定
        if ($this->filled($lower)) {
            $lower->setTime(0, 0, 0);
        }

        // 内閣府からCSVデータを取得
        $response = $this->executeCurlRequest(self::SHUKUJITU_CSV_URL);

        // 文字エンコーディングを変換(SHIFT-JIS → UTF-8)
        $response = mb_convert_encoding($response, 'utf-8', 'SHIFT-JIS');

        // CSVデータをパース
        $holidays_data = explode(PHP_EOL, rtrim($response));
        array_shift($holidays_data); // ヘッダー行を削除

        // 日付フィルタリングとデータ整形
        $filtered_holidays = array_map(function ($item) use ($lower) {
            return $this->parseHolidayData($item, $lower);
        }, $holidays_data);

        // null値を除去して配列を再構築
        $filtered_holidays = array_filter($filtered_holidays, function ($item) {
            return $this->filled($item);
        });

        return array_values($filtered_holidays);
    }

    /**
     * 休日データの1行をパースする
     *
     * @param string $item CSVの1行
     * @param DateTime|null $lower 下限日付
     * @return array|null パースされたデータまたはnull
     */
    private function parseHolidayData(string $item, ?DateTime $lower): ?array
    {
        $parts = explode(",", $item);
        
        if (count($parts) < 2) {
            return null;
        }

        $date = $parts[0]; // 例:1955/1/1
        $holidayName = rtrim($parts[1]);

        // 日付をDateTimeオブジェクトに変換
        $dateTime = DateTime::createFromFormat('Y/n/j', $date);
        if (!$dateTime) {
            return null;
        }
        
        $dateTime->setTime(0, 0, 0);

        // 下限日付でフィルタリング
        if ($this->filled($lower) && $dateTime < $lower) {
            return null;
        }

        return [$date, $holidayName];
    }

    /**
     * 休日データを取得し、追加の休日を設定する
     *
     * @return array 完全な休日データの配列
     */
    private function holidays(): array
    {
        // 内閣府から基本データを取得
        $lowerDate = new DateTime(WITHOUT_LOWER_DATE);
        $holidays = $this->fromCabinetOffice($lowerDate);

        // 追加の休日を設定
        $holidays = array_merge($holidays, $this->getAdditionalHolidays());

        // 日付昇順にソート
        usort($holidays, function ($a, $b) {
            return strtotime($a[0]) - strtotime($b[0]);
        });

        return $holidays;
    }

    /**
     * 追加の休日(大晦日、三が日など)を取得する
     *
     * @return array 追加休日の配列
     */
    private function getAdditionalHolidays(): array
    {
        $currentYear = (int)date('Y');
        $nextYear = $currentYear + 1;

        return [
            // 実行年の大晦日
            [
                date('Y/12/31'),
                '大晦日',
            ],
            // 来年の1/2(三が日)
            [
                "{$nextYear}/1/2",
                '三が日',
            ],
            // 来年の1/3(三が日)
            [
                "{$nextYear}/1/3",
                '三が日',
            ],
        ];
    }

    /**
     * 休日データをデータベースに一括登録・更新する
     *
     * @param array $holidays 休日データの配列
     * @return array APIレスポンス
     * @throws Exception API呼び出しに失敗した場合
     */
    private function upsertHolidays(array $holidays): array
    {
        return SimpleSpiralApiRequest::databaseBulkUpsert([
            'db_title' => self::DB_TITLE,
            'key' => self::KEY_COLUMN,
            'columns' => self::COLUMNS,
            'data' => $holidays,
        ]);
    }

    /**
     * メイン処理を実行する
     * 
     * 1. 内閣府から休日データを取得
     * 2. 追加の休日を設定
     * 3. データベースに登録・更新
     */
    public static function main(): void
    {
        try {
            $service = new self();
            
            echo "休日データの取得を開始します...\n";
            $holidays = $service->holidays();
            
            echo "取得した休日数: " . count($holidays) . "\n";
            
            echo "データベースへの登録を開始します...\n";
            $res = $service->upsertHolidays($holidays);
            
            echo "登録完了!\n";
            var_dump($res);
            
        } catch (Exception $e) {
            echo "エラーが発生しました: " . $e->getMessage() . "\n";
            exit(1);
        }
    }
}


PHPコード(SPIRAL API呼び出し機能)

SPIRAL APIのBulk Upsertを呼び出すPHPスクリプトです。

コピー
<?php
class SimpleSpiralApiRequest
{
    private const LOCATOR_URL = "https://www.pi-pe.co.jp/api/locator";
    private static ?string $apiUrl = null;

    /**
     * APIでlocatorを取得する
     *
     * @return string APIのURL
     * @throws Exception API呼び出しに失敗した場合
     */
    private static function fetchApiUrl(): string
    {
        $api_headers = [
            "X-SPIRAL-API: locator/apiserver/request",
            "Content-Type: application/json; charset=UTF-8",
        ];

        $parameters = [
            "spiral_api_token" => SPIRAL_API_TOKEN
        ];

        $ch = curl_init(self::LOCATOR_URL);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($parameters));
        curl_setopt($ch, CURLOPT_HTTPHEADER, $api_headers);

        $response = curl_exec($ch);

        if (curl_errno($ch)) {
            throw new Exception(curl_error($ch));
        }

        curl_close($ch);

        $response_array = json_decode($response, true);

        if (!isset($response_array['location'])) {
            throw new Exception('Invalid API response: location not found');
        }

        return $response_array['location'];
    }

    /**
     * APIのURLを取得する(既に取得済みの場合はそれを返す)
     *
     * @return string APIのURL
     */
    private static function getApiUrl(): string
    {
        if (is_null(self::$apiUrl)) {
            self::$apiUrl = self::fetchApiUrl();
        }

        return self::$apiUrl;
    }

    /**
     * データベースの一括登録・更新を実行する
     *
     * @param array $param パラメータ
     * @return array APIレスポンス
     * @throws Exception API呼び出しに失敗した場合
     */
    public static function databaseBulkUpsert(array $param): array
    {
        $api_headers = [
            "X-SPIRAL-API: database/bulk_upsert/request",
            "Content-Type: application/json; charset=UTF-8"
        ];

        $time = time();

        // 認証パラメータを構築
        $basicParam = [
            "spiral_api_token" => SPIRAL_API_TOKEN,
            "passkey" => $time,
            "signature" => hash_hmac('sha1', SPIRAL_API_TOKEN . "&" . $time, SPIRAL_API_SECRET, false),
        ];

        $ch = curl_init(self::getApiUrl());

        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array_merge($basicParam, $param)));
        curl_setopt($ch, CURLOPT_HTTPHEADER, $api_headers);

        $response = curl_exec($ch);

        if (curl_errno($ch)) {
            throw new Exception(curl_error($ch));
        }

        curl_close($ch);

        $response_array = json_decode($response, true);

        // エラーチェック
        if (!array_key_exists('code', $response_array) || $response_array['code'] !== "0") {
            throw new Exception('SPIRAL API Error Occurred.[response:' . $response . ']');
        }

        return $response_array;
    }
}

実行&実行結果

以下プログラムにて処理が実行されます。

HolidayRegisterService::main();

スクリプトを実行すると、以下のような出力が表示されます。

コピー
休日データの取得を開始します...
取得した休日数: 25
データベースへの登録を開始します...
登録完了!
array(3) {
  ["code"]=>
  string(1) "0"
  ["message"]=>
  string(2) "OK"
  ["results"]=>
  array(40) {
    [0]=>
    array(2) {
      ["id"]=>
      int(1258)
      ["status"]=>
      string(7) "updated"
    }
    ...
  }
}

実装時の注意点

このスクリプトは、SPIRAL APIの認証情報が必要です。
内閣府のCSVデータはSHIFT-JISエンコーディングで提供されます。
実行前にSPIRAL_API_TOKENとSPIRAL_API_SECRETを設定してください。
データベース名(DB_TITLE)は環境に合わせて変更してください。
日付の下限設定(WITHOUT_LOWER_DATE)を適切に設定してください。

まとめ

本記事では、内閣府から祝日データを自動取得し、SPIRALのデータベースに一括登録するPHPスクリプトを紹介しました。
このスクリプトにより、毎年更新される祝日データを手動で管理する手間を大幅に削減できます。
実行前に適切な認証情報(TOKEN,SECRET)とデータベース設定を行い、定期的な実行(カスタムプログラムによる定期実行)で祝日マスタを最新に保つことをお勧めします。
解決しない場合はこちら コンテンツに関しての
要望はこちら