Google Search Console - v1.0.0
Reporting
The server-side surface for calling Google Search Console. Every UI shipped by this package is a thin renderer over these primitives — you can use them directly from controllers, jobs, artisan commands, or any other server-side context.
Two layers:
- Low level:
SearchAnalyticsClient— a typed wrapper over Google'ssearchAnalytics.queryendpoint. - High level: the three fetchers —
PerformanceOverviewFetcher,TopQueriesFetcher,TopPagesFetcher— that build the exact queries the shipped UIs need and hand back typed DTOs.
Sub-pages:
SearchAnalyticsClient— the client itself.- Fetchers — the three high-level fetchers.
DateRange— the value object every fetcher takes.- Caching — how the client caches, cache keys, when to disable.
Quick reference
use ArtisanPackUI\GoogleSearchConsole\Facades\GoogleSearchConsole;
use ArtisanPackUI\GoogleSearchConsole\Reporting\DateRange;
use ArtisanPackUI\GoogleSearchConsole\Reporting\PerformanceOverviewFetcher;
// 1. Confirm the reporting side is usable.
if ( ! googleSearchConsole()->hasReporting() ) {
// artisanpack-ui/google isn't installed. Bail cleanly.
return;
}
// 2. Resolve the client (singleton).
$client = googleSearchConsole()->client();
// 3. Wrap it in a fetcher for one of the three canned reports.
$fetcher = new PerformanceOverviewFetcher( $client );
$data = $fetcher->fetch( $connection, DateRange::lastDays( 28 ) );
// 4. Read the DTO.
$data->totals; // ['clicks' => 1234.0, 'impressions' => 45678.0, ...]
$data->trend; // list of daily rows
$data->hasData; // false when Google returned zero rows
When to reach for the client directly
SearchAnalyticsClient::query() is the escape hatch. Use it when:
- You need a dimension the fetchers don't cover (
country,device,searchAppearance, or combos like[query, page]). - You want to apply
dimensionFilterGroups(filter by country, page substring, etc.). - You need
searchType(web,image,video,news, …) ordataState(allvsfinal).
Otherwise, the fetchers give you typed DTOs and less boilerplate.
use ArtisanPackUI\GoogleSearchConsole\Reporting\DateRange;
use ArtisanPackUI\GoogleSearchConsole\Reporting\SearchAnalyticsRequest;
$response = googleSearchConsole()->client()->query(
new SearchAnalyticsRequest(
dateRange: DateRange::lastDays( 28 ),
dimensions: [ 'country', 'device' ],
rowLimit: 100,
),
$connection,
);
foreach ( $response->rows() as $row ) {
// $row['country'], $row['device'], $row['clicks'], ...
}
What the client does not do
- Retries. A
502or429from Google surfaces as aReportingException— you decide whether to retry, back off, or give up. Wrap in a queued job with$triesif you want automatic retry. - Rate limiting. Search Console's quotas are per-user + per-property + per-day. The client won't help you stay under them — throttle upstream.
- Pagination beyond
startRow.searchAnalytics.querycaps rowLimit at 25,000 per request; paginate by passingstartRowon subsequent calls if you need the full tail.
Multi-tenant apps
The SearchAnalyticsClient is registered as a singleton with the property URL bound from config. In multi-tenant apps that share one Google account across tenants, override the binding in a tenant-scoped middleware:
namespace App\Http\Middleware;
use ArtisanPackUI\GoogleSearchConsole\Reporting\SearchAnalyticsClient;
use ArtisanPackUI\Google\Tokens\TokenManager;
use Closure;
use Illuminate\Http\Client\Factory as HttpFactory;
class ResolveGscSiteUrl
{
public function handle( $request, Closure $next )
{
$tenant = $request->user()?->currentTenant;
if ( $tenant ) {
config( [
'google-search-console.reporting.site_url' => $tenant->gsc_site_url,
] );
// The singleton was built at boot from the previous config value.
// Rebuild it so the new value is honored.
app()->instance( SearchAnalyticsClient::class, new SearchAnalyticsClient(
config: app( 'config' ),
http: app( HttpFactory::class ),
tokens: app( TokenManager::class ),
cache: app( 'cache.store' ),
) );
}
return $next( $request );
}
}
Register the middleware globally (or on the route group hitting the endpoints) and every fetcher call downstream will use the tenant's property. The three JSON endpoints already ignore any ?site_url query string override, so cross-tenant leakage isn't possible via the URL.
Testing
Every reporting call is HTTP against Google's endpoint, so Http::fake() is enough. See Testing for the full patterns.
use Illuminate\Support\Facades\Http;
Http::fake( [
'*' => Http::response( [
'rows' => [
[ 'keys' => [ 'buy shoes' ], 'clicks' => 42, 'impressions' => 500, 'ctr' => 0.084, 'position' => 3.2 ],
],
], 200 ),
] );
$response = googleSearchConsole()->client()->query( $request, $connection );
expect( $response->rows()[0] )->toMatchArray( [
'query' => 'buy shoes',
'clicks' => 42.0,
] );
Continue to Scopes →