jeffersongoncalves/laravel-google-ads
Laravel package for integrating Google Ads, with configuration and tools to authenticate and interact with the Google Ads API from your application. Simplifies setup and usage within Laravel projects.
This is the initial v1.0.0 release, so treat the public API as young: pin the version ("vendor/google-ads-laravel": "^1.0") and read the README + source before building abstractions on top of it.
Minimal setup:
composer require vendor/google-ads-laravel:^1.0
php artisan vendor:publish --tag=google-ads-config
.env — typically a developer token, OAuth client ID/secret, refresh token, and (for MCC access) a login customer ID:
GOOGLE_ADS_DEVELOPER_TOKEN=
GOOGLE_ADS_CLIENT_ID=
GOOGLE_ADS_CLIENT_SECRET=
GOOGLE_ADS_REFRESH_TOKEN=
GOOGLE_ADS_LOGIN_CUSTOMER_ID=
GoogleAdsClient — check the service provider for the exact binding name) and pass a customer ID without dashes.Where to look first, in order: the package README, config/google-ads.php, the service provider (to see what is bound and whether the client is a singleton), then the GoogleAdsClient class itself for method signatures and return types.
First useful use case — an artisan command that proves credentials work end-to-end:
$client = app(GoogleAdsClient::class);
$account = $client->accountInfo($customerId); // confirms auth + MCC header
$campaigns = $client->campaigns($customerId); // confirms GAQL plumbing
If accountInfo() succeeds but campaigns() returns empty, you almost certainly have a customer-ID or MCC-header problem rather than a code problem (see Gotchas).
Wrap the client behind your own service. Don't scatter app(GoogleAdsClient::class) through controllers. Create a thin GoogleAdsService that injects the client and exposes the handful of operations your app actually performs. This gives you one place to add logging, retries, and account-ID resolution, and one place to adapt if the package's method signatures shift in a 1.x minor.
Lean on raw GAQL for anything not covered. The release covers search, account info, campaigns, ad groups, keywords, and budgets — but reporting needs will outgrow that immediately. Use the search()/GAQL method for one-off queries and keep the query strings in dedicated classes or constants rather than inline heredocs:
$rows = $client->search($customerId, <<<'GAQL'
SELECT campaign.id, campaign.name, metrics.cost_micros, metrics.conversions
FROM campaign
WHERE segments.date DURING LAST_30_DAYS
ORDER BY metrics.cost_micros DESC
GAQL);
Separate read paths from write paths. Listing and performance methods are safe to call from request handlers (with caching). Pause/enable and budget updates are not — route them through queued jobs so a slow or rate-limited Google response never blocks a request, and so retries are handled by the queue rather than the user.
Multi-account / multi-tenant loops. Most agencies and SaaS products manage many customer IDs. Build a repository that iterates accounts, and cache per-account metadata (currency, timezone, name) aggressively — it changes rarely and is needed on nearly every screen:
Cache::remember("gads:account:{$customerId}", now()->addHours(12), fn () =>
$client->accountInfo($customerId)
);
Campaign status changes deserve an audit trail. Pause/enable is a one-line call but an operationally significant action. Wrap it:
DB::transaction(function () use ($client, $customerId, $campaignId) {
$client->pauseCampaign($customerId, $campaignId);
CampaignAudit::create([...]); // who, what, when, previous status
});
Note the transaction only guards your database — if the Google call succeeds and the insert fails, you have a silent divergence. Log the Google response first, then persist.
Budget updates need guardrails, not just a method call. A budget object is shared: any campaign pointing at it changes spend when you update it. Before calling updateBudget(), load the campaigns attached to that budget and require explicit confirmation if more than one is attached. Treat the new amount as a percentage-change check (reject >X% swings without a second approval) — this is the single easiest way to avoid a very expensive typo.
Testing. Don't hit the API in tests. Bind a fake implementation of your own GoogleAdsService interface in the container, and keep a small set of recorded GAQL response fixtures for parsing tests. If the package ships a contract/interface, bind against that instead of the concrete class so you can swap it.
login-customer-id is the classic silent failure. Manager (MCC) accounts must send the manager's customer ID in the header while the request targets the child account. Symptom: USER_PERMISSION_DENIED or empty result sets. Verify the config value is the manager ID, not the child.123-456-7890 is the display format; the API wants 1234567890. Normalise once at your boundary.1000000 micros = 1 unit of the account currency. Mixing units is the most common cause of "why is my budget 1,000,000× too large."->delay() between batches over tight foreach loops.requestId plus the customer ID and the operation, then rethrow a domain exception your app understands.php artisan config:cache, env changes require a re-cache. Credential rotation that "didn't take" is usually this.v1.0.0 means the surface is settled but unproven. Pin to ^1.0 and read the changelog on every minor bump; expect method names around reporting to be the most likely to churn.How can I help you explore Laravel packages today?