revolution/laravel-google-searchconsole
Laravel wrapper for the Google Search Console API. Query search analytics data, inspect sites and sitemaps, and manage properties from your app with a clean, expressive PHP interface built on Google's official client library.
Install the package:
composer require revolution/laravel-google-searchconsole
Laravel's auto-discovery registers the service provider and the LaravelGoogleSearchConsole facade.
Publish the config file (check the README for the exact tag name, typically google-searchconsole-config) and inspect config/google-searchconsole.php. The key setting is the path to your service-account JSON credentials.
In Google Cloud Console:
public/ (e.g. storage/app/google/service-account.json) and point the config/env at its absolute path.In Search Console, open the property → Settings → Users and permissions → Add user, and add the service account's client_email with at least Full or Restricted permission. This step is separate from GCP IAM and is the usual reason the first call fails.
First use case — pull search analytics for a property:
use Revolution\GoogleSearchConsole\Facades\LaravelGoogleSearchConsole as SearchConsole;
$response = SearchConsole::setSite('https://example.com/')
->query([
'startDate' => now()->subDays(28)->toDateString(),
'endDate' => now()->subDays(2)->toDateString(),
'dimensions' => ['query'],
'rowLimit' => 25,
]);
foreach ($response->getRows() ?? [] as $row) {
dump($row->getKeys(), $row->getClicks(), $row->getImpressions());
}
Where to look first: the published config file, then the facade class (src/Facades/) and its resolved service class to confirm the exact method names (setSite, query, sites, sitemaps, etc.). The underlying return objects are Google\Service\SearchConsole\* classes from google/apiclient, so the Google PHP client docs and the Search Console API reference are the real API surface.
Wrap the facade in your own service. Don't sprinkle the facade through controllers. Create App\Services\SearchConsoleService that injects the config, holds a default site, and returns typed DTOs/arrays. This isolates the third-party API and makes it swappable in tests.
Scheduled sync into your database. The most common production pattern: a daily job that pulls the last N days and upserts into a search_console_rows table keyed by (site, date, query, page, country, device). Later, dashboards read from your DB instead of hitting the API. Keep the sync window overlapping (e.g. re-pull 7 days) because Google backfills data.
// app/Console/Commands/SyncSearchConsole.php
foreach (config('searchconsole.sites') as $site) {
SearchConsole::setSite($site)->query([
'startDate' => now()->subDays(10)->toDateString(),
'endDate' => now()->subDays(2)->toDateString(),
'dimensions' => ['date','query','page','country','device'],
'rowLimit' => 25000,
'startRow' => 0,
]);
}
Paginate manually. rowLimit caps at 25,000 per request. Loop with startRow until fewer than rowLimit rows come back. A page-level loop is usually cheaper than splitting by date.
Discover properties dynamically. Use the sites/sitemaps endpoints to enumerate verified properties once, then iterate. This avoids hardcoding sc-domain: vs https:// mismatches.
Cache aggressively for interactive pages. Analytics data is delayed anyway; a 15–60 minute cache (Cache::remember) on any dashboard request saves quota and latency. Prefer syncing to your DB for anything heavier.
Multiple credentials / multi-tenant. If you serve agencies, build one service instance per credential set. The config holds a single path by default, so construct the underlying Google client yourself for tenant-specific credentials rather than mutating global config at runtime.
Testing. Because the package resolves a google/apiclient service object, bind a mock or use an HTTP-level fake against www.googleapis.com. For unit tests, swapping the facade with SearchConsole::shouldReceive(...) is the fastest path.
403 until the service account email is a user on the property. This is the #1 support question.https://example.com/ (trailing slash), https://www.example.com/, and sc-domain:example.com are three distinct properties. Store the canonical form in config; don't derive it from APP_URL.endDate = today returns empty or partial data. Always end at now()->subDays(2).startDate/endDate are strings (Y-m-d), not Carbon objects — cast with ->toDateString().query + page together inflates rows fast; filter with dimensionFilterGroups instead of aggregating in PHP.base_path('storage/...') works; a relative path silently fails inside queued workers with a different CWD. Set GOOGLE_SEARCHCONSOLE_CREDENTIALS env and reference it in config.sleep() between them.getRows(), getKeys(), getClicks(), etc. — not arrays. The response can be null-ish for rows when no data exists; guard with ?? [].last_synced_at column.Google\Client. If a needed endpoint (e.g. newer Search Console features) isn't exposed, resolve the underlying Google client from the container and call the service directly — you don't need to fork the package.Google\Service\Exception, log getErrors() and the site string; generic 500 errors from Google are almost always a mismatched site or missing permission.How can I help you explore Laravel packages today?