moirei/laravel-google-merchant-api
Integrate Google Merchant Center into your Laravel app with this API wrapper. Manage merchant accounts, product listings, and feeds using a clean, expressive PHP interface, with Laravel-friendly configuration and service account authentication.
The package is a thin Laravel-friendly wrapper around Google's Merchant (Content) API for managing Google Shopping product listings. Minimal setup:
composer require moirei/laravel-google-merchant-api
php artisan vendor:publish --tag=google-merchant-api-config
Then add your Google service account credentials and Merchant Center ID to .env:
GOOGLE_MERCHANT_ACCOUNT_ID=123456789
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
Service accounts must be granted access in the Merchant Center UI (Users → add the service account email as a "Standard" or "Admin" user). Without that step, every API call returns 401 and it can look like a package misconfiguration.
First use case — pushing a single product. Resolve the client from the container and insert a product. Most developers start here to prove credentials work:
use Moirei\GoogleMerchantApi\Facades\Product;
Product::insert([
'offerId' => 'SKU-001',
'title' => 'Blue T-shirt',
'description' => 'A cotton tee',
'link' => 'https://shop.test/p/1',
'imageLink' => 'https://shop.test/img/1.jpg',
'contentLanguage' => 'en',
'targetCountry' => 'US',
'channel' => 'online',
'condition' => 'new',
'availability' => 'in stock',
'price' => ['value' => '19.99', 'currency' => 'USD'],
]);
Look first at config/google-merchant-api.php to confirm the credential path and account ID are wired, then check the package's src/ for the facade/resource classes (Product, Products, or similar) — that's the real surface area you will be working with.
Facade for quick scripts, DI for testable services. Use the facade in Artisan commands and one-off scripts. In services and controllers, inject the underlying client or product resource so you can mock it in tests.
Sync from an Eloquent model via observers or an explicit action. Keep the "push to Google" logic in a dedicated class rather than sprinkling it through controllers:
class SyncProductToGoogle
{
public function __construct(private Products $products) {}
public function handle(Product $product): void
{
$this->products->update([
'offerId' => $product->sku,
'title' => $product->name,
'availability' => $product->stock > 0 ? 'in stock' : 'out of stock',
'price' => [
'value' => number_format($product->price, 2, '.', ''),
'currency' => 'USD',
],
// ...other fields
]);
}
}
Wire it from a model observer's updated event or a queued job so HTTP requests to Google don't block your checkout or admin save.
Bulk operations belong in queues. Google's API is slow and rate-limited. Batch products into a job, chunk with ->chunk(50, ...), and dispatch with a rate limiter (Redis::throttle('merchant-api')->allow(10)->every(1)). Don't loop inside a synchronous controller.
Map to your catalog domain, not Google's schema. Build a toGoogleProduct() mapper per product type (physical, variant, digital). Google's schema is verbose and strict; keeping a single translation seam makes validation errors readable.
Feed-style reconciliation. For large catalogs, use list to pull existing products, diff against local DB, and issue insert / update / delete only where needed. A last_synced_at column on your product model plus a google_status field makes this a cheap check.
Testing. Fake the client by binding a mock of the resource interface into the container in your test's setUp. Assert on the payloads passed to insert/update rather than hitting Google. Keep a small fixture file for response snapshots.
GOOGLE_APPLICATION_CREDENTIALS is resolved from the process working directory — which is base_path() under Artisan/web but may differ under Octane, Sail, or containerized workers. Use absolute paths or base_path() inside config.contentLanguage~targetCountry~offerId. When you update or delete, you need to reconstruct this — the package usually handles it, but keep contentLanguage and targetCountry stable or you'll orphan products.merchantId at the top level targets the multi-client account. Most single-store setups need the sub-account ID. Verify in Merchant Center → Settings.Google\Service\Exception (or GuzzleHttp\Exception\RequestException). Log $e->getMessage() and the decoded body — Google returns field-level paths (item[0].price.value) that tell you exactly what's wrong.channel, contentLanguage, targetCountry, condition, availability, price, shipping, tax — miss one and the product is silently "pending" or rejected. Enable Google Merchant Center diagnostics in the UI alongside API responses.google/apiclient dependency caches OAuth tokens in memory per request. In long-running workers (Horizon, Octane), recreate the client occasionally or inject a fresh one per job to avoid stale token errors.403 rateLimitExceeded. Back off with exponential retry (retry(5) on the job) and throttle the dispatch.logging to the config (or enable Guzzle debug via handler) to dump request/response bodies; Google's errors are opaque without the raw payload.Google\Client and service resources via the container. Override the binding in a service provider to swap in a custom Client — useful for proxies, retries, or stubbing in tests..gitignore, store in a secret manager, and reference via env. Rotating the key requires clearing any cached tokens.src/ and the package tests; treat the vendor code as the source of truth for method signatures rather than blog posts.How can I help you explore Laravel packages today?