PHP

PHPのstrtotimeでAPIデータを期間抽出し、対象データをDBへ保存する方法

外部APIから取得した複数件の日付付きデータをstrtotimeで比較し、指定期間内のデータだけを抽出してDBへ保存する流れを、注意点とともに解説します。

この記事でわかること

  • APIから取得した日付文字列を比較可能な値へ変換する流れを理解できる
  • 開始日・終了日を含む期間条件の組み立て方を確認できる
  • 不正な日付やfalseの扱い、DB保存時の実務上の注意点を把握できる
  • DateTimeやDateTimeImmutableを使う場合との使い分けを検討できる

大量のAPIデータを必要な期間だけ保存する

外部APIから日付付きのデータをまとめて取得しても、取得した全件をDBへ保存する必要があるとは限りません。たとえば、集計対象を2026年1月1日から2026年1月31日までに限定したい場合、APIレスポンスをそのまま保存すると不要なデータまで蓄積されます。このような処理では、API取得、日付変換、期間判定、対象データの抽出、DB保存という順番で処理を組み立てます。API側に期間指定のパラメータがある場合は先に利用する方が効率的ですが、APIの仕様上すべて取得してから絞り込む必要がある場合は、PHP側で日付を比較します。

  1. APIから複数件のデータを取得する
  2. 日付文字列をstrtotime()で変換する
  3. 開始日以上、終了日以下かを判定する
  4. 条件に一致したデータだけをDBへ保存する

APIレスポンスと期間指定の前提を決める

ここでは、APIレスポンスをJSONデコードした後の配列を扱います。実際のAPIに存在するフィールド名や日付形式はサービスごとに異なるため、以下の例ではitem_id、name、published_atというフィールドを仮定します。日付はISO 8601形式の文字列で、タイムゾーンを含む前提です。

PHP
<?php

$apiData = [
    ['item_id' => 'A001', 'name' => '商品A', 'published_at' => '2025-12-31T23:00:00+09:00'],
    ['item_id' => 'A002', 'name' => '商品B', 'published_at' => '2026-01-01T00:00:00+09:00'],
    ['item_id' => 'A003', 'name' => '商品C', 'published_at' => '2026-01-15T12:30:00+09:00'],
    ['item_id' => 'A004', 'name' => '商品D', 'published_at' => '2026-01-31T23:59:59+09:00'],
    ['item_id' => 'A005', 'name' => '商品E', 'published_at' => '2026-02-01T00:00:00+09:00'],
];

$startDate = '2026-01-01';
$endDate = '2026-01-31';

// この記事では開始日・終了日の両方を含む範囲として扱う。

開始日・終了日を日付単位で扱う場合は、両端を含む条件にするのかを先に決めます。上の例では2026年1月1日と2026年1月31日のデータを対象にします。時刻付きデータを扱う場合は、終了日の00:00:00をそのまま上限にすると、終了日当日の昼や夜のデータが対象外になる点に注意が必要です。

strtotimeでAPIの日付文字列を比較できる値へ変換する

APIの日付が文字列の場合、文字列のまま比較するのではなく、データ側と期間側を同じ基準で変換して比較します。strtotime()は日付時刻の文字列をUnixタイムスタンプへ変換し、変換できない場合はfalseを返します。したがって、戻り値を0などと区別できるよう、厳密比較で確認します。

PHP
<?php

$startTimestamp = strtotime($startDate . ' 00:00:00');
$endTimestamp = strtotime($endDate . ' 23:59:59');

if ($startTimestamp === false || $endTimestamp === false) {
    throw new RuntimeException('期間指定の日付を解釈できません。');
}

$filteredData = [];

foreach ($apiData as $item) {
    $publishedTimestamp = strtotime($item['published_at'] ?? '');

    if ($publishedTimestamp === false) {
        // 実運用ではitem_idや元の値をログへ記録してスキップする。
        continue;
    }

    if ($publishedTimestamp >= $startTimestamp
        && $publishedTimestamp <= $endTimestamp) {
        $filteredData[] = $item;
    }
}

// 期待される対象ID: A002, A003, A004

APIの日付形式が想定と違う場合、strtotime()の解釈に任せて保存するのではなく、API仕様を確認したうえでエラー記録やスキップの方針を決めます。特に、形式が固定されていない入力では、日付として解釈できても意図と異なる日付になる可能性があります。タイムゾーンを含む入力では、API、PHP実行環境、DBの基準をそろえてください。

開始日・終了日を含む条件で対象データを抽出する

期間内を表す条件は、対象日時が開始日時以上かつ終了日時以下です。比較演算子に>=と<=を使うことで、開始日当日と終了日当日のデータも含められます。日付単位で判定する要件なら、終了日を23:59:59まで広げる方法が分かりやすい一方、マイクロ秒を含むデータやUTC変換を行うシステムでは、終了日翌日の00:00:00を含まない半開区間、つまり「開始日以上かつ終了日翌日未満」とする設計も検討できます。

PHP
<?php

$startTimestamp = strtotime('2026-01-01 00:00:00');
$endExclusiveTimestamp = strtotime('2026-02-01 00:00:00');

$publishedTimestamp = strtotime($item['published_at'] ?? '');

if ($publishedTimestamp !== false
    && $publishedTimestamp >= $startTimestamp
    && $publishedTimestamp < $endExclusiveTimestamp) {
    // 2026年1月1日から1月31日までを対象にする。
}

この半開区間では、2026年2月1日00:00:00は対象外になり、2026年1月31日23:59:59のデータは対象になります。時刻の精度やタイムゾーンを考慮しやすいため、時刻付きデータではこの形式が扱いやすい場合があります。

抽出したデータをDBへ保存する

抽出後は、必要な項目をDBのカラムへ対応付けて保存します。以下はPDOと名前付きプレースホルダを使う例です。SQLへ値を直接連結せず、prepare()とexecute()で値を渡します。PHPの公式マニュアルでも、ユーザー入力をクエリへ直接含めず、パラメータマーカーを利用する方法が説明されています。 テーブル名やカラム名、INSERT時の重複処理はDB製品と既存スキーマに合わせて変更してください。

PHP
<?php

$pdo = new PDO(
    'mysql:host=localhost;dbname=example;charset=utf8mb4',
    'app_user',
    'password',
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]
);

$sql = <<<SQL
INSERT INTO api_items (external_id, name, published_at)
VALUES (:external_id, :name, :published_at)
SQL;

$stmt = $pdo->prepare($sql);

foreach ($filteredData as $item) {
    if (!isset($item['item_id'], $item['name'], $item['published_at'])) {
        // 必須項目が欠けたデータは保存しない。
        continue;
    }

    $stmt->execute([
        'external_id' => $item['item_id'],
        'name' => $item['name'],
        'published_at' => $item['published_at'],
    ]);
}

// 実行環境のDB定義に応じて、日時型や文字列長を調整する。

APIの識別子であるexternal_idを保存し、DB側で一意制約を設けると、同じAPIデータを再取得したときの重複対策を検討しやすくなります。単純なINSERTでよいのか、既存行を更新するUPSERTにするのかは、再実行時の要件によって決めます。

API取得からDB保存までを一つの処理に組み立てる

実務では、API通信の失敗、JSONデコードの失敗、日付変換の失敗、DB保存の失敗を別々に扱えるようにします。複数件を一括処理する場合、途中で保存に失敗したときに一部だけ登録されることを避けたいなら、DBのトランザクションを利用します。

PHP
<?php

$startDate = '2026-01-01';
$endDate = '2026-01-31';

$startTimestamp = strtotime($startDate . ' 00:00:00');
$endExclusiveTimestamp = strtotime($endDate . ' 00:00:00 +1 day');

if ($startTimestamp === false || $endExclusiveTimestamp === false) {
    throw new RuntimeException('期間指定が不正です。');
}

// 実際にはHTTPクライアントでAPIへ接続し、JSON文字列を取得する。
$json = '{"items":[
    {"item_id":"A002","name":"商品B","published_at":"2026-01-01T00:00:00+09:00"},
    {"item_id":"A003","name":"商品C","published_at":"2026-01-15T12:30:00+09:00"},
    {"item_id":"A004","name":"商品D","published_at":"2026-01-31T23:59:59+09:00"}
]}';

$payload = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
$items = $payload['items'] ?? [];

$pdo->beginTransaction();

try {
    $stmt = $pdo->prepare(
        'INSERT INTO api_items (external_id, name, published_at)
         VALUES (:external_id, :name, :published_at)'
    );

    foreach ($items as $item) {
        $publishedTimestamp = strtotime($item['published_at'] ?? '');

        if ($publishedTimestamp === false) {
            // 日付変換失敗はログへ記録してスキップする。
            continue;
        }

        if ($publishedTimestamp < $startTimestamp
            || $publishedTimestamp >= $endExclusiveTimestamp) {
            continue;
        }

        $stmt->execute([
            'external_id' => $item['item_id'],
            'name' => $item['name'],
            'published_at' => $item['published_at'],
        ]);
    }

    $pdo->commit();
    // 期待される挙動: 指定期間内のデータだけが登録される。
} catch (Throwable $e) {
    $pdo->rollBack();
    // 実運用では例外内容を記録し、再試行や通知の方針を決める。
    throw $e;
}

上のコードは処理の組み立てを示すサンプルであり、API通信部分、PDO接続、テーブル定義は環境に合わせて実装します。ページングや取得件数の上限があるAPIでは、対象期間の全ページを取得できているかも確認が必要です。対象外データを破棄するだけでなく、件数や除外理由をログへ記録すると、API仕様変更の検知に役立ちます。

実務で起きやすい日付と重複保存の問題に備える

  • タイムゾーンを統一する。APIのオフセット、PHPのデフォルトタイムゾーン、DBの保存基準が異なると、同じ瞬間が別の日として判定される可能性がある。
  • 日付変換失敗を検知する。スキップした件数、外部ID、元の日付文字列をログへ残すと、API仕様変更を追いやすい。
  • ページングを確認する。1回のレスポンスだけで指定期間の全データを取得できるとは限らない。
  • 重複保存を防ぐ。外部IDの一意制約、UPSERT、保存前の照合などから要件に合う方法を選ぶ。
  • 必須項目を検証する。フィールド欠損、NULL可否、文字列長、日時型の範囲を保存前に確認する。

DateTime・DateTimeImmutableを使う場合との使い分け

単純な日時文字列を変換して比較するだけなら、strtotime()は短く書きやすい選択肢です。ただし、strtotime()が返すUnixタイムスタンプ自体にはタイムゾーン情報が含まれないため、日時の計算やタイムゾーン管理が複雑になる場合はDateTime系を検討します。

PHP
<?php

$timezone = new DateTimeZone('Asia/Tokyo');
$start = new DateTimeImmutable('2026-01-01 00:00:00', $timezone);
$endExclusive = $start->modify('+1 month');

$published = DateTimeImmutable::createFromFormat(
    DateTimeInterface::ATOM,
    $item['published_at'] ?? ''
);

if ($published !== false
    && $published >= $start
    && $published < $endExclusive) {
    // DateTimeImmutableでは変更操作の結果として新しいオブジェクトを扱う。
}

DateTimeImmutableは、日時を変更する操作で元のオブジェクトを変更せず、新しいオブジェクトを返します。期間計算や複数の基準日時を扱う処理では、元の値を保ちやすい点が利点です。

参考