LaravelとGA4 連携

すでにGA4の設定をGCPの設定済みと仮定して、Laravelでの実装のみのアプトプットになります。
もし、それまでの設定が必要な場合は下記の記事を参考にしてくだしい。
https://www.fourier.jp/blog/ga4-with-php

ライブラリーをインストール
```
composer require google/analytics-data
```

これも追加
```
composer require google/apiclient
```

JSONファイルをstoragaディレクトリー配置する必要がある
(gitignore設定は必須)

### しかしエラー
```
Class "Google\Analytics\Data\V1beta\BetaAnalyticsDataClient" not found
```
ライブラリーは正しく存在されていることを確認
```
composer show | grep google/analytics-data
google/analytics-data              0.22.0  Google Analytics Data Client for PHP
```

### 解決
パスが変わってたみたい(下記が新しい名前空間です)

```
use Google\Analytics\Data\V1beta\Client\BetaAnalyticsDataClient;
```

bcmathが必要とエラーが出たのでDockerfileに追加
```
RUN apt-get update && apt-get install -y \
  git \
  unzip \
  libpq-dev \
  libzip-dev \
  && docker-php-ext-install pdo pdo_mysql zip bcmath
```

これで使えるようになった
```
 public function handle()
    {
        $propertyId = config('google.analytics.propertyId');
        $credentialsPath = config('google.analytics.credentials');

        $client = new BetaAnalyticsDataClient([
            'credentials' => $credentialsPath,
            'transport'   => 'rest',
        ]);

        $request = new RunReportRequest([
            'property'   => 'properties/' . $propertyId,
            'date_ranges' => [
                new DateRange([
                    'start_date' => '30daysAgo',
                    'end_date'   => 'today',
                ]),
            ],
            'dimensions' => [
                new Dimension(['name' => 'pagePath']),
                new Dimension(['name' => 'pageTitle']),
            ],
            'metrics' => [
                new Metric(['name' => 'screenPageViews']),
            ],
            'limit' => 100,
        ]);

         $request->setReturnPropertyQuota(true);//  使用制限を確認する
          // dd($response->getPropertyQuota());

        $response = $client->runReport($request);

        foreach ($response->getRows() as $row) {
            $dimensionValues = $row->getDimensionValues();
            $metricValues    = $row->getMetricValues();

            $pagePath  = $dimensionValues[0]->getValue();
            $pageTitle = $dimensionValues[1]->getValue();
            $pageViews = $metricValues[0]->getValue();

            $this->info("Path: $pagePath | Title: $pageTitle | PV: $pageViews");
        }

    }
```

ここからcronで時間で自動的に実行されるようにする

メモ
job_batches　テーブルが作成されたがこれは何？


laravel11からはroutes/console.logに記述が必要になったみたい
https://readouble.com/laravel/11.x/ja/scheduling.html

一旦下記のように記述してみたがうまくいかない
```
use Illuminate\Support\Facades\Schedule;
Schedule::command('analitics-command')->everyMinute() //毎分実行;
```

しかし、```php artisan schedule:list```をして確認すると実行はされているっぽい
なぜか,ログには実行内容が記述されない
```
php artisan schedule:list

  0 * * * *  php artisan inspire ........................................................................................................................................ Next Due: 14分後
  * * * * *  php artisan analitics-command .............................................................................................................................. Next Due: 56秒後
```

下記のコマンドを実行すると処理は実行されるが根本的な問題は解決していない
```
 php artisan schedule:run
 ```

 ### 解決
 cron をインストールが必要
 一旦コンテナに直でインストール
 ```
 apt-get install cron
 ```

 これがないとcron　ファイルを操作できない

 ```
apt-get install -y nano
 ```

cron 設定ファイルを修正する必要があるため、ファイルを開く
 ```
 crontab -e
 ```
 Laravelのスケジュールコマンドを毎分実行する設定を追加します。(ここはパスによって変更が必要(
 ```
* * * * * cd /var/www/html && /usr/local/bin/php artisan schedule:run >> /dev/null 2>&1
 ```

 cronサービスを起動
 ```
 service cron start
 ```

 実際に動いているか確認
 ```
 service cron status
cron is running.
 ```

 時間通りに処理もうまくいっている

 #### リアルタイム(30分)のごとの値を取得する
 ```
 <?php

namespace App\Console\Commands\GoogleAnalytics;

use Illuminate\Console\Command;

use Google\Analytics\Data\V1beta\Client\BetaAnalyticsDataClient;

// リアルタイム レポート関連クラス
use Google\Analytics\Data\V1beta\RunRealtimeReportRequest;
use Google\Analytics\Data\V1beta\Dimension;
use Google\Analytics\Data\V1beta\Metric;

class RealTimeReportCommand extends Command
{
    /**
     * The name and signature of the console command.
     */
    protected $signature = 'app:real-time-report-command';

    /**
     * The console command description.
     */
    protected $description = 'GA4のリアルタイムレポート (runRealtimeReport) でデータを取得する';

    public function handle()
    {
        // 1) 環境変数や config などから GA4 プロパティID と 認証JSONパスを取得
        $propertyId = config('google.analytics.propertyId');   // 例: 123456789
        $credentialsPath = config('google.analytics.credentials'); // 例: storage_path('service-account.json')

        // 2) BetaAnalyticsDataClient のインスタンスを生成
        //    'credentials' にサービスアカウントJSONのパスを指定
        $client = new BetaAnalyticsDataClient([
            'credentials' => $credentialsPath,
        ]);

        try {
            // 3) リアルタイム レポート用リクエストを作成
            //    ※ "property" は "properties/123456789" の形で設定する
            //    ※ 取得したい dimension / metric に注意
            $request = new RunRealtimeReportRequest([
                'property' => "properties/{$propertyId}",
                'dimensions' => [
                    // new Dimension(['name' => 'eventName']),
                    new Dimension(['name' => 'unifiedScreenName']), // 記事名
                ],
                'metrics' => [
                    // new Metric(['name' => 'activeUsers']),
                    new Metric(['name' => 'screenPageViews']), // PV数
                ],
                'limit' => 10,
            ]);

            // 4) 実際に runRealtimeReport を呼び出し
            $response = $client->runRealtimeReport($request);

            // 5) レスポンスを表示
            $this->info("---- GA4 Realtime Report ----");
            foreach ($response->getRows() as $row) {
                // dd($row);
                $dimensions = $row->getDimensionValues();
                $metrics = $row->getMetricValues();

                $unifiedScreenName = $dimensions[0]->getValue() ?? '(no event)';
                $screenPageViews = $metrics[0]->getValue() ?? 0;
                // $activeUsers = $metrics[1]->getValue() ?? 0;
                // $eventName = $metrics[2]->getValue() ?? '(no event)';


                $this->info("unifiedScreenName: {$unifiedScreenName}, screenPageViews: {$screenPageViews}");

            }
        } catch (\Exception $e) {
            $this->error("Error: " . $e->getMessage());
        }

        return 0;
    }
}

 ```


 runReportの場合
 ```
 <?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Carbon\Carbon;
use Google\Analytics\Data\V1beta\Client\BetaAnalyticsDataClient;
use Google\Analytics\Data\V1beta\RunReportRequest;
use Google\Analytics\Data\V1beta\Filter;
use Google\Analytics\Data\V1beta\Filter\StringFilter\MatchType;
use Google\Analytics\Data\V1beta\FilterExpression;
use Google\Analytics\Data\V1beta\OrderBy;
use Google\Analytics\Data\V1beta\OrderBy\DimensionOrderBy;
use Google\Analytics\Data\V1beta\DateRange;
use Google\Analytics\Data\V1beta\Dimension;
use Google\Analytics\Data\V1beta\Metric;
use Google\Service\Dfareporting\Order;
use Illuminate\Support\Facades\DB;

class RunReport extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:run-report';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $yesterday = Carbon::yesterday()->format('Y-m-d');

        $propertyId = config('google.analytics.propertyId');
        $credentialsPath = config('google.analytics.credentials');

        $client = new BetaAnalyticsDataClient([
            'credentials' => $credentialsPath,
            'transport'   => 'rest',
        ]);

        $request = new RunReportRequest([
            'property'   => 'properties/' . $propertyId,
            'date_ranges' => [
                new DateRange([
                    'start_date' => $yesterday,
                    'end_date'   => $yesterday,
                ]),
            ],
            'dimensions' => [
                new Dimension(['name' => 'pagePathPlusQueryString']),
            ],
            'metrics' => [
                new Metric(['name' => 'screenPageViews']),
            ],
            'order_bys' => [
                new OrderBy([
                    'dimension' => new DimensionOrderBy(['dimension_name' => 'pagePathPlusQueryString']),
                    'desc'   => false,
                ]),
            ],
            'limit' => 10,
        ]);

        // $request->setReturnPropertyQuota(true);//  使用制限を確認する

        $response = $client->runReport($request);
        $posts = DB::connection('wordpress')->table('wp_posts')->where('post_type', 'post')->where('post_status', 'publish')->pluck('ID')->toArray();

        $pv_Array = [];
        // 存在する記事のIDのみを保存する
        foreach ($response->getRows() as $row) {
            $dimensionValues = $row->getDimensionValues();
            $metricValues    = $row->getMetricValues();
            $pagePathPlusQueryString = $dimensionValues[0]->getValue();
            $pageViews = $metricValues[0]->getValue();


            $postId = $this->getPostIdFromPagePath($pagePathPlusQueryString, $posts);

            if ($postId) {
                $pv_Array[] = [
                    'post_id' => $postId,
                    'pv_count' => $pageViews,
                    'target_date' => $yesterday,
                    'created_by' => '日時記事アクセス集計バッチ',
                    'updated_by' => '日時記事アクセス集計バッチ',
                    'created_at' => now(),
                    'updated_at' => now(),
                ];
            }

            // エピソードに存在するwordpressの記事のIDのみPVを保存する
            \Log::info("post_id: $postId | pagePathPlusQueryString: $pagePathPlusQueryString |  PV: $pageViews");
            $this->info("post_id: $postId | pagePathPlusQueryString: $pagePathPlusQueryString |  PV: $pageViews");
        }

        // if (!empty($pv_Array)) {
        //     DB::table('posts_pv')->insert($pv_Array);
        // }
    }

    /**
 * pagePathPlusQueryStringからpost_idを取得する関数
 */
    function getPostIdFromPagePath($pagePathPlusQueryString, $posts):int | null
    {
       preg_match('/\/\?p=(\d+)$/', $pagePathPlusQueryString, $matches);
    //    dd($matches, $posts, gettype($posts));

       // 記事IDが存在する場合はそのIDを返す
       if (!empty($matches[1]) && in_array($matches[1], $posts)) {
         return $matches[1];
        }
        return null;
    }
}

```


### Dockerコンテナ間通信方法

#### 1 共有ネットワークを作成する
```
docker network create -d bridge local-cms-network
```
#### 2 共通ネットワークが作成されたか確認
```
docker inspect local-cms-network 
```

#### 3 docker-compose.ymlに共通ネットワークを記載(両方のymlに記載)
```
# それぞれのサービスに記載
networks:
      - cms-network
# 最後に記載
networks:
  cms-network:
    external: true
    name: local-cms-network

```

#### 4 docker再起動
```
docker compose up -d --build

```


### Laravelでレスポンスボディを取得する方法
```
$jsonString = $response->serializeToJsonString();
dd($jsonString);
```

#### ディメンションをそれぞれ取得する方法(ページスクリーン/ブラウザ)
```
<?php

namespace App\Console\Commands\GoogleAnalytics;

use Illuminate\Console\Command;
use Carbon\Carbon;
use Google\Analytics\Data\V1beta\Client\BetaAnalyticsDataClient;
use Google\Analytics\Data\V1beta\RunPivotReportRequest;
use Google\Analytics\Data\V1beta\DateRange;
use Google\Analytics\Data\V1beta\Dimension;
use Google\Analytics\Data\V1beta\Metric;
use Google\Analytics\Data\V1beta\Pivot;
use Exception;
use Illuminate\Support\Facades\Log;

class RunPivotReportCommand extends Command
{
    protected $signature = 'schedule:run-pivot-report';
    protected $description = 'GA4でpagePathPlusQueryString×browserのピボットを取得する';

    public function handle(): int
    {
        try {
            $client  = $this->createGa4Client();
            $request = $this->createRunPivotReportRequest();
             $request->setReturnPropertyQuota(true);//  使用制限を確認する
            $response = $client->runPivotReport($request);

            $jsonString = $response->serializeToJsonString();
            // dd($response->getPropertyQuota());
            // dd($jsonString);
            // レスポンスを表示
            $this->displayPivotResult($response);


        } catch (Exception $e) {
            Log::error($e->getMessage());
            $this->error($e->getMessage());
            return Command::FAILURE;
        }

        Log::info('正常に終了しました');
        return Command::SUCCESS;
    }

    private function createGa4Client(): BetaAnalyticsDataClient
    {
        return new BetaAnalyticsDataClient([
            'credentials' => config('google.analytics.credentials'),
            'transport'   => 'rest',
        ]);
    }

    private function createRunPivotReportRequest(): RunPivotReportRequest
    {
        return new RunPivotReportRequest([
            'property' => 'properties/' . config('google.analytics.propertyId'),
            // 計測期間
            'date_ranges' => [
                new DateRange([
                    'start_date' => '2025-02-25',
                    'end_date'   => '2025-02-25',
                ])
            ],

            // 2つのディメンション
            'dimensions' => [
                new Dimension(['name' => 'pagePathPlusQueryString']),
                new Dimension(['name' => 'browser']),
            ],

            // 取得したい指標
            'metrics' => [
                new Metric(['name' => 'activeUsers']),
            ],

            // 2つのピボットオブジェクトを定義
            // 1. pagePathPlusQueryString
            // 2. browser
            // これによって GA4 の「2次元ピボット」として認識されます。
            'pivots' => [
                new Pivot([
                    'field_names' => ['pagePathPlusQueryString'],
                    'limit'       => 50,
                ]),
                new Pivot([
                    'field_names' => ['browser'],
                    'limit'       => 10,
                ]),
            ],

        ]);

    }

    private function displayPivotResult($response): void
    {
        // pivotHeaders は、pivots で定義した順に返る
        //   $pivotHeaders[0] => pagePathPlusQueryString 用のヘッダ
        //   $pivotHeaders[1] => browser 用のヘッダ
        $pivotHeaders = $response->getPivotHeaders();

        // 1つめのピボット (行方向) の dimension 値一覧
        // たとえば p?=13, p?=14 など
        $pagePathHeader = $pivotHeaders[0];
        $pagePathValues = [];
        foreach ($pagePathHeader->getPivotDimensionHeaders() as $pdh) {
            // dimensionValues は通常1要素だが、複数の場合もある
            $val = $pdh->getDimensionValues()[0]->getValue();
            $pagePathValues[] = $val;
        }

        // 2つめのピボット (列方向) の dimension 値一覧
        // たとえば Chrome, Firefox, Safari 等
        $browserHeader = $pivotHeaders[1];
        $browserValues = [];
        foreach ($browserHeader->getPivotDimensionHeaders() as $pdh) {
            $val = $pdh->getDimensionValues()[0]->getValue();
            $browserValues[] = $val;
        }

        // 実際のデータ行
        // pivot 2次元の場合、各 row は「pagePathの値, browserの値」と metrics がセットになって返る
        // → dimensionValues[0] => 例: "p?=13"
        //    dimensionValues[1] => 例: "Chrome"
        //    metricValues       => 例: activeUsers=1
        // ただし GA4ピボットは「1セルごとに1行」で返るため、行×列形式にするには再構築が必要
        $resultsMatrix = []; // $resultsMatrix[pagePath][browser] = activeUsers

        foreach ($response->getRows() as $row) {
            $dims = $row->getDimensionValues();  // [0]: pagePathPlusQueryString, [1]: browser
            $mets = $row->getMetricValues();     // [0]: activeUsers

            $pagePath = $dims[0]->getValue();
            $browser  = $dims[1]->getValue();
            $activeUsers = (int)$mets[0]->getValue();

            $resultsMatrix[$pagePath][$browser] = $activeUsers;
        }

        // 以下は画面出力用の例 (行が pagePathPlusQueryString, 列が browser)
        // まずブラウザの見出し行を表示
        $this->line("pagePathPlusQueryString\t" . implode("\t", $browserValues));

        // 各pagePath行を表示
        foreach ($pagePathValues as $pp) {
            // 行の左端に pagePath
            $rowOutput = [$pp];
            // その列のブラウザ順に値を取り出す
            foreach ($browserValues as $br) {
                $val = $resultsMatrix[$pp][$br] ?? 0;
                $rowOutput[] = $val;
            }
            $this->line(implode("\t", $rowOutput));
        }
    }
}

```