GitHubActionsでGA4データを取得してLaravelに自動保存してみた2

2025.10.03 09:00
2025.09.29 10:13
GitHubActionsでGA4データを取得してLaravelに自動保存してみた2

あああ

1. Laravel側の受け口

まずは API のルートを用意します。
/api/ga4 に POST できるようにして、ヘッダには X-Job-Token を付けて認証する仕組みにしました。

<?php

Route::middleware('job.token')->group(function () {
    Route::post('/ga4', Ga4StoreController::class)->name('ga4.store');
});

2. バリデーション(FormRequest)

POSTされる内容は「site_id / month / preset / url / scores…」とシンプルなので、
FormRequest にルールをまとめました。

class Ga4StoreRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'site_id' => ['required', 'integer', 'exists:sites,id'],
            'month' => ['required', 'date_format:Y-m-d'],

            'summary' => ['required', 'array'],
            'summary.pageviews' => ['required', 'integer'],
            'summary.users' => ['required', 'integer'],
            'summary.sessions' => ['required', 'integer'],
            'summary.engagement_rate' => ['required', 'numeric'],

            'devices' => ['required', 'array'],
            'devices.*.device' => ['required', 'string'],
            'devices.*.users' => ['required', 'integer'],

            'channels' => ['required', 'array'],
            'channels.*.channel' => ['required', 'string'],
            'channels.*.users' => ['required', 'integer'],

            'top_pages' => ['required', 'array'],
            'top_pages.*.url' => ['required', 'string'],
            'top_pages.*.pageviews' => ['required', 'integer'],
        ];
    }
}

3. 保存処理(Controller → UseCase)

コントローラでは Request を受けて、UseCaseに丸投げする形にしました。
保存は updateOrCreate にして、同じ月・同じURLは上書きするようにしています。
DB保存処理は割愛します。

final class Ga4StoreController extends Controller
{
    public function __invoke(
        Ga4StoreRequest $request,
        Ga4StoreUseCase $useCase
    ): JsonResponse {
        $item = $useCase($request->validated());

        return response()->json([
            'message' => GA4 report stored successfully',
            'data'    => $item,
        ], 201);
    }
}

DBはこんな感じ。

Schema::create('ga4_reports', function (Blueprint $table) {
    $table->id();

    // 外部キー
    $table->foreignId('site_id')->constrained('sites')->cascadeOnDelete();

    $table->date('month')->comment('レポート対象月 (YYYY-MM-01)');
    $table->json('summary')->default(DB::raw('(JSON_ARRAY())'))->comment('サマリー(アクセス数/ユーザー数など)');
    $table->json('devices')->default(DB::raw('(JSON_ARRAY())'))->comment('デバイス別');
    $table->json('channels')->default(DB::raw('(JSON_ARRAY())'))->comment('流入チャネル');
    $table->json('top_pages')->default(DB::raw('(JSON_ARRAY())'))->comment('人気ページ');
    $table->timestamps();

    // 重複登録防止
    $table->unique(['site_id','month']); // サイト×月でユニークに
    $table->index(['site_id', 'month']); // サイト×月でindexを貼る
});

4. X-Job-Tokenを設定する

外部(GitHub Actions)から直接APIを叩くので、そのまま公開すると誰でもPOSTできてしまいます。
そこで Laravel 側に 専用トークン認証 を入れておきます。
ヘッダに X-Job-Token を付けて送信し、Laravelで .env に置いた値と突き合わせてチェックするだけのシンプルな仕組みです。

ちなみにこの工程は前回のLighthouseCIで行っていた場合は不要です。

<?php declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

final class VerifyJobToken
{
    public function handle(Request $request, Closure $next): Response
    {
        $expected = (string) config('jobs.token', '');
        $given    = (string) $request->header('X-Job-Token', '');

        if ($expected === '' || !hash_equals($expected, $given)) {
            return response()->json(['message' => 'Unauthorized'], 401);
        }

        return $next($request);
    }
}

上のミドルウェアを登録します。

上部割愛〜

    // インストールしたMiddlewareを指定
    ->withMiddleware(function (Middleware $middleware) {

        // 明示的に指定した時に反映
        $middleware->alias([
            'job.token' => VerifyJobToken::class,
        ]);

以降割愛〜

設定ファイルにキーを置きます。

return [
    // ...
    'job_token' => env('JOBS_TOKEN'),
];

.envにも追加しておきます。

JOBS_TOKEN=your-super-secret-token

これでトークンを持ったアクセスのみをAPIで受けられるようになりました!

5. まとめ

GA4・Lighthouse・E2E と、種類の違うデータを扱う API でも、コントローラの書き方を揃えておくと見通しがよくなります。

入力の整形や補完は Request 側に寄せておけば、UseCase はドメイン処理に集中でき、Controller も薄く保てます。結果として「どの API も同じ型で動く」安心感が生まれ、保守や追加実装もスムーズになります。

こちらも残りはダッシュボードなどのView側を残すのみですね。
今回は以上です!