はじめに
LaravelからFirebase(Firestore)に接続し、定期的な処理をタスクスケジューラー(Laravelのスケジューラー)で実行する方法をまとめました。
今回は以下のようなユースケースを想定します:
- Firestoreに保存されたデータを定期的に読み取って処理
- 条件に合致するデータに対してメール通知やフラグ更新などのバッチ処理
- GCP SDKではなく、REST API経由でアクセス(Laravelから簡単に呼び出す)
LaravelとFirebaseの相性について
LaravelはもともとMySQLやPostgreSQLなどのRDBと親和性の高いPHPフレームワークですが、Firebase(特にFirestore)はNoSQL型のスキーマレスなドキュメントDBです。
このため、Eloquent ORMなどLaravel標準のDB操作機能とは異なるアプローチが求められます。しかし、以下の点でFirebaseとLaravelは十分に組み合わせ可能です:
- Laravelの柔軟なサービス構造とDI(依存性注入)でFirebase SDKやREST APIのラップが容易
- LaravelのスケジューラーやQueueなどのバックグラウンド処理機能とFirestoreのイベント的なデータ構造が親和性あり
- Firebase AuthenticationやStorageとの連携もHTTPベースで行えるため、LaravelのHTTPクライアントで対応可能
注意点:
- リアルタイム同期やトリガーベースの処理(Cloud Functionsなど)については、LaravelよりもNode.jsやFirebase自身との相性が良い
- LaravelからFirestoreを使う場合、SDKベースよりREST APIベースの方が手軽(ただし制限もある)
構成概要
- Laravel 10.x
- Firebase(Firestore)
- Laravelの
Schedule
機能(cron) - Guzzle HTTP client使用
2025年5月時点での私が利用しているLaravelのバージョン
とcomposer.json(一部)
はこちら
% php artisan --version
Laravel Framework 10.48.28
"require": {
"php": "^8.1",
"google/auth": "^1.47",
"guzzlehttp/guzzle": "^7.2",
"kreait/firebase-php": "^7.16",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8"
}
※バージョンは作業時に各自で確認してください。
ステップ1: Firebaseの認証情報を取得
まずはFirebaseのプロジェクトを作成し秘密鍵を取得します。
Firebaseはこちらからアクセスできます。
- Firebase Console → 「プロジェクトの設定」
- 「サービスアカウント」 → 新しい秘密鍵を生成
- ダウンロードしたJSONを保存
ステップ2: ステップ2: LaravelでFirestoreクライアントを設定
guzzlehttp/guzzle
をプロジェクトに追加します。
composer require guzzlehttp/guzzle
app/Services/FirestoreService.php
を作成します。
<?php
namespace App\Services;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Storage;
use DateTime;
class FirestoreService
{
private $client;
private $projectId;
public function __construct()
{
$credentials = json_decode('ダウンロードしたJSONファイル', true);
$this->projectId = $credentials['project_id'];
$this->client = new Client([
'base_uri' => 'FirestoreのベースURL',
'headers' => ['Content-Type' => 'application/json'],
]);
}
private function getAccessToken()
{
$credentialsPath = storage_path('ダウンロードしたJSONファイル');
$url = 'https://oauth2.googleapis.com/token';
$scope = 'https://www.googleapis.com/auth/datastore';
$credentials = json_decode(file_get_contents($credentialsPath), true);
$client = new \GuzzleHttp\Client();
$response = $client->post($url, [
'form_params' => [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $this->generateJwtAssertion($credentials, $scope),
]
]);
return json_decode($response->getBody(), true)['access_token'];
}
private function generateJwtAssertion($credentials, $scope)
{
$header = base64_encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));
$now = time();
$claimSet = base64_encode(json_encode([
'iss' => $credentials['client_email'],
'scope' => $scope,
'aud' => 'https://oauth2.googleapis.com/token',
'exp' => $now + 3600,
'iat' => $now,
]));
// 秘密鍵を正しい形式に変換
$privateKey = $credentials['private_key'];
$privateKey = str_replace("\\n", "\n", $privateKey);
$privateKeyResource = openssl_pkey_get_private($privateKey);
if ($privateKeyResource === false) {
throw new \Exception('Invalid private key: Unable to parse');
}
$signature = '';
openssl_sign("$header.$claimSet", $signature, $privateKeyResource, 'sha256WithRSAEncryption');
$signature = base64_encode($signature);
return "$header.$claimSet.$signature";
}
/**
* データをFirestoreのフォーマットに変換
*/
private function formatData(array $data)
{
$formatted = ['fields' => []];
foreach ($data as $key => $value) {
if (is_string($value)) {
$formatted['fields'][$key] = ['stringValue' => $value];
} elseif (is_int($value)) {
$formatted['fields'][$key] = ['integerValue' => $value];
} elseif (is_float($value)) {
$formatted['fields'][$key] = ['doubleValue' => $value];
} elseif (is_bool($value)) {
$formatted['fields'][$key] = ['booleanValue' => $value];
} elseif (is_array($value)) {
$formatted['fields'][$key] = ['arrayValue' => ['values' => array_map(fn($v) => ['stringValue' => $v], $value)]];
} elseif ($value instanceof DateTime) {
$formatted['fields'][$key] = ['timestampValue' => $value->format(DateTime::ATOM)];
}
}
return $formatted;
}
/**
* Firestore JSON形式に変換
*/
private function formatFirestoreFields(array $data)
{
$fields = [];
foreach ($data as $item) {
$path = $item['path'];
$value = $item['value'];
if (is_string($value)) {
$fields[$path] = ['stringValue' => $value];
} elseif (is_int($value)) {
$fields[$path] = ['integerValue' => $value];
} elseif (is_float($value)) {
$fields[$path] = ['doubleValue' => $value];
} elseif (is_bool($value)) {
$fields[$path] = ['booleanValue' => $value];
} elseif (is_array($value)) {
$fields[$path] = ['arrayValue' => ['values' => array_map(fn($v) => ['stringValue' => $v], $value)]];
}
}
return $fields;
}
/**
* ドキュメントを取得
*/
public function getDocument($collection, $document)
{
$accessToken = $this->getAccessToken();
$response = $this->client->get("$collection/$document", [
'headers' => ['Authorization' => "Bearer {$accessToken}"],
]);
return json_decode($response->getBody(), true);
}
/**
* ドキュメントを条件付きで取得 (where句)
*/
public function getDocumentsWhere($collection, array $conditions)
{
$accessToken = $this->getAccessToken();
// where条件をJSON構造に変換
$structuredQuery = [
'structuredQuery' => [
'from' => [['collectionId' => $collection]],
'where' => $this->buildWhereClause($conditions)
]
];
$response = $this->client->post(":runQuery", [
'headers' => ['Authorization' => "Bearer {$accessToken}"],
'json' => $structuredQuery,
]);
return array_map(fn($doc) => $doc['document'] ?? null, json_decode($response->getBody(), true));
}
/**
* where条件を構築
*/
private function buildWhereClause(array $conditions)
{
$clauses = [];
foreach ($conditions as $condition) {
$clauses[] = [
'fieldFilter' => [
'field' => ['fieldPath' => $condition['field']],
'op' => $this->convertOperator($condition['operator']),
'value' => $this->formatWhereValue($condition['value'])
]
];
}
return count($clauses) > 1
? ['compositeFilter' => ['op' => 'AND', 'filters' => $clauses]]
: $clauses[0];
}
/**
* Firestoreの比較演算子に変換
*/
private function convertOperator($operator)
{
return match ($operator) {
'=' => 'EQUAL',
'>' => 'GREATER_THAN',
'>=' => 'GREATER_THAN_OR_EQUAL',
'<' => 'LESS_THAN',
'<=' => 'LESS_THAN_OR_EQUAL',
'!=' => 'NOT_EQUAL',
'array-contains' => 'ARRAY_CONTAINS',
'in' => 'IN',
'array-contains-any' => 'ARRAY_CONTAINS_ANY',
default => throw new \InvalidArgumentException("Unsupported operator: $operator")
};
}
/**
* Firestore用にwhereの値をフォーマット
*/
private function formatWhereValue($value)
{
return match (true) {
is_string($value) => ['stringValue' => $value],
is_int($value) => ['integerValue' => $value],
is_float($value) => ['doubleValue' => $value],
is_bool($value) => ['booleanValue' => $value],
is_array($value) => ['arrayValue' => ['values' => array_map(fn($v) => ['stringValue' => $v], $value)]],
default => throw new \InvalidArgumentException("Unsupported value type")
};
}
/**
* ドキュメント追加
*/
public function addDocument($collection, array $data)
{
$accessToken = $this->getAccessToken();
$response = $this->client->post($collection, [
'headers' => ['Authorization' => "Bearer {$accessToken}"],
'json' => $this->formatData($data), // フォーマット済みのデータを送信
]);
return json_decode($response->getBody(), true);
}
/**
* ドキュメントを更新 (部分更新 = merge)
*/
public function updateDocument($collection, $document, array $data)
{
$accessToken = $this->getAccessToken();
// 更新するフィールド名を取得 (クエリパラメータ用に)
$fieldPaths = array_map(fn($item) => $item['path'], $data);
// FirestoreのJSON形式に変換
$fields = [];
foreach ($data as $item) {
$path = $item['path'];
$value = $item['value'];
if (is_string($value)) {
$fields[$path] = ['stringValue' => $value];
} elseif (is_int($value)) {
$fields[$path] = ['integerValue' => $value];
} elseif (is_float($value)) {
$fields[$path] = ['doubleValue' => $value];
} elseif (is_bool($value)) {
$fields[$path] = ['booleanValue' => $value];
} elseif (is_array($value)) {
$fields[$path] = ['arrayValue' => ['values' => array_map(fn($v) => ['stringValue' => $v], $value)]];
} elseif ($value instanceof DateTime) {
$fields[$path] = ['timestampValue' => $value->format(DateTime::ATOM)];
}
}
// クエリパラメータを複数回設定
$query = [];
foreach ($fieldPaths as $field) {
$query[] = 'updateMask.fieldPaths=' . urlencode($field);
}
$url = "$collection/$document?" . implode('&', $query);
$response = $this->client->patch($url, [
'headers' => ['Authorization' => "Bearer {$accessToken}"],
'json' => ['fields' => $fields],
]);
return json_decode($response->getBody(), true);
}
/**
* ドキュメントを削除
*/
public function deleteDocument($collection, $document)
{
$accessToken = $this->getAccessToken();
$this->client->delete("$collection/$document", [
'headers' => ['Authorization' => "Bearer {$accessToken}"],
]);
return true;
}
}
上記コードでデータ取得
,データ追加
,データ更新
,データ削除
が可能になります。
ステップ3: コマンドの作成
実際にFirebase(Firestore)にアクセスして処理を行うコードを実装します。
php artisan make:command ProcessFirestoreTasks
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\FirestoreService;
class ProcessFirestoreTasks extends Command
{
protected $signature = 'firebase:process-tasks';
protected $description = 'Firestoreのタスクを処理';
private FirestoreService $firestore;
public function __construct(FirestoreService $firestore)
{
parent::__construct();
$this->firestore = $firestore;
}
public function handle(FirestoreService $firestore)
{
// データ取得
$result = $this->firestore->getDocumentsWhere('コレクション', 'コレクションID');
// データ取得(条件付き)
$documents = $this->firestore->getDocumentsWhere('コレクション', [
['field' => 'フォールド名', 'operator' => '条件(>や=、>=など)', 'value' => '値'],
]);
$result = collect($documents)->map(function ($doc) {
return [
'uid' => $doc['fields']['uid']['stringValue'] ?? null,
'value' => $doc['fields']['value']['stringValue'] ?? null,
];
})->toArray();
// データ追加
$this->firestore->addDocument('コレクション', [
"フォールド名" => "値",
]);
// データ更新
$this->firestore->updateDocument('コレクション', 'コレクションID', [
['path' => 'フォールド名', 'value' => '値'],
]);
// データ削除
$this->firestore->deleteDocument('コレクション', 'コレクションID');
}
}
Laravel(php)からFirebaseのようなNoSQLを触る時、UpdateやWhere句を使うときは少し書き方が変わるので注意が必要です。
ステップ4: スケジューラーに登録
実装した処理をタスクスケジューラーに追加します。
protected function schedule(Schedule $schedule)
{
$schedule->command('firebase:process-tasks')->everyMinute();
}
タスクスケジューラーを利用できるようcron
を設定します。
* * * * * cd /path/to/laravel && php artisan schedule:run >> /dev/null 2>&1
終わりに
LaravelとFirebaseは、構造的には異なるものの、REST API経由で連携すれば十分に実用的な構成が可能です。
Laravel側にタスク処理を任せることで、Cloud Functionsに頼らずとも柔軟な運用が可能になります。