Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Shopping Merchant Products Laravel Package

google/shopping-merchant-products

PHP client for the Google Shopping Merchant Products API. Create, update, list, and delete Merchant Center product inputs, manage attributes and data sources, and integrate product catalog workflows into Laravel or any PHP app via generated Google API classes.

View on GitHub
Deep Wiki
Context7

Getting Started

google/shopping-merchant-products is an auto-generated GAPIC client — it gives you typed PHP classes for one slice of the Merchant API (products), not a high-level "Merchant Center SDK." Expect the ergonomics of a protobuf client, not of a hand-written library.

Install and authenticate

composer require google/shopping-merchant-products
  • PHP 8.1+, google/gax, google/protobuf come in transitively. ext-grpc is optional — you can run entirely on REST.
  • Local dev: gcloud auth application-default login (ADC just works, no code changes).
  • Production: point GOOGLE_APPLICATION_CREDENTIALS at a service account JSON that has been added as a user on the Merchant Center account, and grant the scope https://www.googleapis.com/auth/content.

Where to look first

  1. The generated client itself: src/V1/Client/ProductsServiceClient.php. Its method docblocks are the only real documentation — they contain the resource-name format, required fields, and a runnable snippet per method.
  2. src/V1/Resources/Product.php — the full field surface of a product, with getters/setters for everything (offerId, contentLanguage, feedLabel, channel, attributes, productStatus, …).
  3. src/V1/Requests/ — one class per RPC telling you exactly which parameters are accepted.
  4. metadata/ — the descriptor files, useful when the docblocks are terse.

First real call — list what's in an account:

use Google\Shopping\Merchant\Products\V1\Client\ProductsServiceClient;
use Google\Shopping\Merchant\Products\V1\ListProductsRequest;

$client  = new ProductsServiceClient(); // ADC
$request = new ListProductsRequest(['parent' => 'accounts/1234567']);

foreach ($client->listProducts($request)->iterateAllElements() as $product) {
    printf("%s  %s\n", $product->getName(), $product->getProductStatus()->getDestinationStatuses()->count());
}

That single snippet is 80% of what most teams do with this package on day one: enumerate a catalog and read per-destination approval status.

Implementation Patterns

Respect the read/write split. In the Merchant API, reads return a Product, writes go through a ProductInput. They are different resources with different services. insertProduct/updateProduct/deleteProduct operate on inputs; getProduct/listProducts read the rendered product that Google produces after processing. Teams waste a day trying to round-trip a Product into an update — you can't. Treat inputs as your "intent" record and products as the "observed" record.

Wrap it, don't sprinkle it. The client is stateless and cheap to construct, but you don't want accounts/1234567 literals or request objects in controllers. A thin domain service pays for itself:

final class MerchantCatalog
{
    public function __construct(
        private ProductsServiceClient $products,
        private ProductsServiceClient $inputs, // or ProductInputsServiceClient, see below
        private string $account,
    ) {}

    public function find(string $offerId, string $lang, string $feedLabel): ?Product
    {
        try {
            return $this->products->getProduct(new GetProductRequest([
                'name' => sprintf('accounts/%s/products/%s~%s~%s', $this->account, $lang, $feedLabel, $offerId),
            ]));
        } catch (ApiException $e) {
            if ($e->getStatus() === Code::NOT_FOUND) {
                return null;
            }
            throw $e;
        }
    }
}

Bind it as a singleton in a service provider — the underlying gRPC channel is reused, so you avoid re-handshaking on every request.

Pagination: never hand-roll pageToken. Every list RPC returns a PagedResponse from google/gax:

$paged = $client->listProducts($request);
foreach ($paged->iteratePages() as $page => $items) { /* batched logging / progress */ }
foreach ($paged->iterateAllElements() as $product) { /* simple fan-out */ }

iterateAllElements() re-issues requests lazily, so it respects the default retry policy; iteratePages() is what you want if you need progress reporting or a time budget.

Bulk writes: use async + a concurrency cap. GAPIC generates *Async twins returning GuzzleHttp\Promise\PromiseInterface. For a feed of a few thousand SKUs:

use Google\ApiCore\Utils\Utils;

$promises = [];
foreach ($chunks as $chunk) {
    $promises[] = $client->insertProductInputAsync($request);
    if (count($promises) >= 20) {
        Utils::all($promises)->wait();   // drain
        $promises = [];
        usleep(250_000);                 // stay under quota
    }
}
Utils::all($promises)->wait();

Wrap the whole thing in a queued job; the Merchant API per-account QPS limits will bite you long before PHP does.

Updates: always send an explicit field mask. Partial updates with a wrong or missing mask are the most common source of "it wiped my attributes" tickets. Build the mask from the fields you actually changed:

$input = (new ProductInput())->setName($name)->setAttributes($attrs);
$client->updateProductInput(new UpdateProductInputRequest([
    'productInput' => $input,
    'updateMask'   => FieldMask::fromString('attributes.title,attributes.description'),
]));

Force REST when gRPC isn't available. Shared hosting, some Docker images, and most serverless PHP runtimes lack ext-grpc. Pass 'transport' => 'rest' in the client options; the wire format changes but the PHP API is identical. This is also your escape hatch for HTTP-level observability (see below).

Pair it with siblings. products alone can't tell you which account to query or how a product performed. In practice you install google/shopping-merchant-accounts (resolve the merchant ID) and google/shopping-merchant-reports (performance metrics) alongside it, and join on offerId/name. A Product object contains destination statuses but no clicks, impressions, or price benchmarks.

Gotchas and Tips

The resource name is the API contract, and it's built from three identifiers. For v1 the canonical form is accounts/{account}/products/{contentLanguage}~{feedLabel}~{offerId} (older/beta surfaces use contentLanguage/feedLabel as separate request fields). Getting the tilde order wrong yields a clean-looking NOT_FOUND with no hint. Centralize name construction in one helper and unit-test it — it is the single highest-value test you can write against this package.

channel is required on inserts and is easy to forget. ONLINE, LOCAL, and PUBLICATIONS behave very differently, and the same offerId can legitimately exist under different channels. A "duplicate product" error is almost always a channel/language/feed mismatch, not a real duplicate.

Retries are pre-configured and not always safe. The generated retrySettings retry on UNAVAILABLE/DEADLINE_EXCEEDED for reads, which is fine. For inserts and updates, decide deliberately: an at-least-once retry on a non-idempotent insert can create conflicting inputs. The pragmatic pattern is insert-then-getProduct to reconcile, or disable retries on write RPCs via retrySettings in the client options.

ApiException is your only error type. Everything — auth failure, quota, 404, validation — surfaces as Google\ApiCore\ApiException. Branch on $e->getStatus() against Google\Rpc\Code::* constants, not on message strings. For 400s, $e->getDetails() sometimes carries the field-level reason; when it doesn't, the message body is the only clue, so log the whole exception including getMetadata() (which carries request-id — hand that to Google support).

Quota project matters in multi-tenant apps. If you bill quota against a host project rather than the service account's project, set the x-goog-user-project header (via credentialsConfig / QUOTA_PROJECT env) or you'll see RESOURCE_EXHAUSTED attributed to the wrong tenant and be unable to debug it.

Two namespaces ship in the box. Google\Shopping\Merchant\Products\V1 and ...\V1beta are entirely distinct class trees; mixing them produces confusing "method not found" errors because the object shapes are near-identical. Pin your imports at the top of one file and never import both.

Debugging wire traffic. gRPC transport gives you almost nothing. If you need to see request payloads, switch to 'transport' => 'rest' and pass a custom Guzzle handler through transportConfig:

new ProductsServiceClient([
    'transport' => 'rest',
    'transportConfig' => ['rest' => ['httpHandler' => $stack]],
]);

This is also the only practical way to add tracing spans around individual RPCs.

Don't treat "2 stars, 0 dependents" as a red flag — treat it as "no community support." Nobody is filing issues on this repo; it is regenerated from the upstream proto on a schedule. Consequences: no changelog you can reason about beyond release tags, breaking changes can appear between minor versions when the proto moves, and the README examples may lag the code. Pin an exact version in composer.json and read git diff on the generated Product.php when you bump.

The Product object is deep and mostly empty. getAttributes() returns a nested graph where nearly every getter returns null or an empty repeated field depending on category. Never assume a field exists; the Merchant API populates only what applies to that product's category and channel. Defensive ?-> chains are normal here, not a smell.

Extension points are thin by design. There's no event system, no middleware in the Laravel sense, no repository interface to swap. Your seams are: (a) subclass the client to override the endpoint for a sandbox, (b) wrap it in your own interface so tests can fake it, (c) pass custom credentials/transportConfig. If you need something richer — caching, metrics, retry policy per call — build it above the client, not inside it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib