widop/google-analytics
A PHP client for querying Google Analytics data via the Core Reporting API, with OAuth2 authentication and simple requests for metrics, dimensions, filters, and segments from your analytics profiles.
What it is. A thin, framework-agnostic PHP wrapper around the Google Analytics Core Reporting API (v3) that handles OAuth2 service-account (certificate) authentication, so you can run reports from cron jobs, dashboards, and back-office tools without a browser or a refresh-token dance.
Install and orient.
composer require widop/google-analytics
Read the README first, then src/. Four classes carry the whole package: Client (credentials/auth), GoogleAnalytics (transport, query()), Query (fluent request builder), and Response (payload accessors). The tests are the de-facto documentation for supported parameter names.
One-time Google setup — do this before writing code:
[email protected]) and add it as a user on the GA view with Read & Analyze.First use case — top 10 countries, last 30 days:
use Widop\GoogleAnalytics\Client;
use Widop\GoogleAnalytics\GoogleAnalytics;
use Widop\GoogleAnalytics\Query;
$client = new Client($clientId, $serviceAccountEmail, '/path/to/key.p12');
$analytics = new GoogleAnalytics($client);
$query = (new Query())
->setIds('ga:12345678') // view ID, not property ID
->setStartDate(new \DateTime('-30 days'))
->setEndDate(new \DateTime('yesterday'))
->setMetrics(['ga:sessions'])
->setDimensions(['ga:country'])
->setSorts(['-ga:sessions'])
->setMaxResults(10);
foreach ($analytics->query($query)->getRows() as $row) {
printf("%s: %d\n", $row[0], $row[1]);
}
If this returns rows, everything downstream (key, scope, permissions, view ID) is correct. Prove that path end-to-end before building any abstraction on top.
Bind one service, configure per environment. Don't scatter new Client(...) across the codebase. A single factory bound to config (client ID, service-account email, key path, view ID, default segment/filters) gives you one place to swap credentials per environment and one seam to mock in tests.
Layer reusable query builders. Most reporting code differs only in metrics, dimensions, and date range. Start from a baseQuery() that sets ids, date bounds, and sampling level, then specialize:
$q = $this->baseQuery($from, $to)
->setMetrics(['ga:pageviews', 'ga:uniquePageviews'])
->setDimensions(['ga:pagePath'])
->setFilters('ga:pagePath=~/blog/')
->setMaxResults(500);
Normalize rows once, immediately. Response mirrors the JSON payload: rows are positional arrays in the order of dimensions then metrics. Convert to associative arrays using the column headers at the boundary and pass only those inward; never let positional indexes leak into templates or exports.
Paginate explicitly. The API caps max-results; walk start-index until you've collected totalResults, or set a deliberate cap for dashboards. There's no batching here — N reports means N round trips, which makes caching mandatory rather than optional.
Cache by query hash. Key on a hash of the serialized query parameters with a TTL matching your reporting cadence (hourly for ops, daily for marketing). Per-project quotas are generous for one dashboard and trivially exhausted by per-user report rendering.
Classify failures at one choke point. 401 = key/scope; 403 = service account missing from the view (by far the most common); 400 = invalid metric/dimension combination or malformed filter; 429 = quota. Always log the raw query string — for this library it's the only artifact you need to reproduce a bug.
Test at the GoogleAnalytics seam. Return canned Response objects from a stub and unit-test your mapping, filtering, and caching logic. Keep one integration test against a real view, marked/skipped by default.
Isolate for migration. Treat this as a legacy-UA adapter. Put it behind your own repository interface so a later swap to the GA4 Data API (google/analytics-data) is a one-class change rather than a rewrite of every caller.
notasecret). Google now issues JSON keys. If the constructor asks for a path plus password, either convert the key or patch the auth layer; don't quietly hand it a JSON file.ga:VIEW_ID, not property ID. The ids parameter takes the numeric view (profile) ID — not the UA property ID, not the GA4 property ID. Mixing these up is the second-most-common failure after permissions.https://www.googleapis.com/auth/analytics.readonly. Anything broader will still authenticate and should fail review.- prefix (-ga:sessions); filters use GA's mini-language (; = AND, , = OR, =~ = regex). Don't hand-roll URL encoding if the client already does it.\DateTime; interpretation follows the view's timezone, so "yesterday" shifts across regions.google/apiclient. Budget for a fork — the extension points are just subclassing Query/Response or replacing Client, so forking is cheap.How can I help you explore Laravel packages today?