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

Google Merchant Feed Laravel Package

vitalybaev/google-merchant-feed

Generate Google Merchant Center product feeds in Laravel with this package. It builds valid XML feeds from your products, supports required attributes, and integrates cleanly into existing Eloquent models and scheduled jobs.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require vitalybaev/google-merchant-feed

Requires PHP 7.1+ and ext-dom / ext-simplexml (both ship with most PHP builds; php -m | grep -E 'dom|simplexml').

First look

  • README.md — one working example gets you 90% of the way.
  • src/Feed.php — the container: addProduct(), getProducts(), build().
  • src/Product.php and src/BaseProduct.php — the attribute setters (setId, setTitle, setPrice, setAvailability, setBrand, setGtin, …) plus addCustomAttribute() for anything the package doesn't model explicitly.

Minimal working feed

use Vitalybaev\GoogleMerchant\Feed;
use Vitalybaev\GoogleMerchant\Product;

$feed = new Feed('My Store', 'https://example.com', 'A great store');

$product = new Product();
$product->setId('SKU-123');
$product->setTitle('Blue Widget');
$product->setDescription('A widget that is blue.');
$product->setLink('https://example.com/p/sku-123');
$product->setImageLink('https://example.com/img/sku-123.jpg');
$product->setAvailability('in stock');
$product->setPrice('19.99', 'USD');
$product->setBrand('Acme');
$product->setGtin('00012345600012');
$product->setCondition('new');

$feed->addProduct($product);

$xml = $feed->build(); // or echo / write to file

First real use case: an Artisan command (php artisan merchant:feed) that writes the XML to storage/app/merchant/feed.xml, plus a route that serves it with Content-Type: application/xml. Register the URL in Google Merchant Center as a scheduled fetch. That single loop — model → Product → build() → file — is the core of every deployment of this package.

Implementation Patterns

Mapper class per model

Don't scatter setTitle() calls through controllers. Extract a ProductMapper:

class ProductMapper
{
    public function fromModel(Item $item): Product
    {
        $p = new Product();
        $p->setId($item->sku);
        $p->setTitle(Str::limit($item->name, 150, ''));
        $p->setDescription(Str::limit(strip_tags($item->description), 5000, ''));
        $p->setLink(route('product.show', $item));
        $p->setImageLink($item->primaryImageUrl());
        $p->setAvailability($item->stock > 0 ? 'in stock' : 'out of stock');
        $p->setPrice(number_format($item->price, 2, '.', ''), 'USD');
        $p->setBrand($item->brand);
        $p->setGtin($item->gtin);
        $p->setCondition('new');

        if ($item->sale_price) {
            $p->setSalePrice(number_format($item->sale_price, 2, '.', ''), 'USD');
        }

        $p->addCustomAttribute('google_product_category', $item->google_category);
        $p->addCustomAttribute('product_type', $item->product_type);

        return $p;
    }
}

Chunked iteration for large catalogs

Feed holds every Product in memory until build(). For 50k+ SKUs, use chunkById and build multiple feed files (Google accepts multiple feed files per target country), or bypass build() and stream with XMLWriter past a threshold.

Item::query()->chunkById(5000, function ($items) use ($feed) {
    foreach ($items as $item) {
        $feed->addProduct($this->mapper->fromModel($item));
    }
});

Serve vs. generate

  • Serve on-demand (route → build() → response) for small catalogs.
  • Generate to disk on a schedule for large ones. Serving the same 80MB XML on every Google fetch is wasteful and slow; let Google fetch a static file behind your CDN.
Route::get('/merchant/feed.xml', function () {
    return response($feed->build(), 200, ['Content-Type' => 'application/xml']);
});

Or write once and let nginx serve it:

Storage::disk('local')->put('merchant/feed.xml', $feed->build());

Custom attributes for spec coverage gaps

Google's spec has hundreds of attributes; this package models the common ones. When Google adds a new field, or you need one the package doesn't implement, addCustomAttribute('name', 'value') emits <g:name> without waiting for a package release. That's your forward-compatibility escape hatch.

Testing

$xml = $feed->build();
$this->assertXmlStringEqualsXmlString(
    '<?xml version="1.0"?><rss xmlns:g="http://base.google.com/ns/1.0">...</rss>',
    $xml
);

assertXmlStringEqualsXmlString ignores whitespace, which saves you from brittle assertions over the indentation build() uses.

Wrapper service

Bind a MerchantFeedBuilder in the container that composes Feed + mappers. If the package is eventually abandoned (it's quiet — last release Dec 2022), you swap the implementation inside the service without touching call sites.

Gotchas and Tips

API drift between versions. Older releases used Offer + addItem() + getXml(); newer ones use Product + addProduct() + build(). If a tutorial doesn't match, check composer.lock. Pin the version in composer.json; the docs on the repo reflect the current tag, not whatever you installed two years ago.

Always pass price as a string with an explicit decimal point. setPrice(19.99, 'USD') may render 19.99 USD fine in one locale and 19,99 in another depending on how the float is cast. Use number_format($price, 2, '.', ''). Same for sale price, shipping, and tax.

Required minimum for Google. At minimum: id, title, description, link, image_link, availability, price, and either brand + gtin/mpn, or identifier_exists=false. Missing any of these and Merchant Center silently disables the item — the feed still generates valid XML, so nothing fails until diagnostics show up in the dashboard. Validate against the Google Merchant Center "Diagnostics" after every schema change.

Enum values are case- and space-sensitive. availability must be exactly in stock, out of stock, preorder, or backorder. condition must be new, refurbished, or used. Title-cased "In Stock" silently rejects.

XML escaping is handled, but not normalization. SimpleXML escapes &, <, >, but it does not strip newlines, control characters, or HTML from your description. strip_tags() and preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $text) before setting descriptions — Google rejects whole feeds over invalid characters.

No schema validation. The package builds well-formed XML but has no idea whether it's valid Google XML. If you want a safety net, validate once in CI with xmllint --schema against Google's XSD, or run the generated file through a merchant-center sanity script.

Memory. Feed keeps Product objects alive until build(). Each Product holds a SimpleXMLElement (or attribute array depending on version) — budget roughly a few KB per SKU. At 100k SKUs you'll hit PHP's default memory_limit; set -d memory_limit=1G on the CLI job or stream instead. Google's own limits are ~1GB uncompressed / 1M items per feed file, so plan file splitting around that.

Duplicate IDs. If two Products share an id, Google discards one; the package won't warn you. Assert uniqueness in the mapper or a test.

Shipping and tax are nested. Multi-shipment products need repeated setShipping(...) style calls (each produces a <g:shipping> block). Read Product.php before assuming single-argument helpers. Verify the emitted XML has one <g:shipping> per shipment — silent overwrites are common.

Currency and units. setPrice takes the currency as an ISO 4217 code; it is not inferred from the store's locale. If you support multiple storefronts, generate one feed per target country/currency rather than mixing.

Date attributes need ISO 8601. sale_price_effective_date, availability_date, expiration_date all expect YYYY-MM-DDThh:mm:ss±hh:mm (or a start/end range for effective dates). Use Carbon::toIso8601String() — not toDateTimeString().

Extension point: subclass Product. For repeated attributes across every SKU, subclass and populate defaults in the constructor. Cleaner than a map() loop and easier to test in isolation.

Debugging an unexpectedly rejected feed: dump the XML, open it in a browser or xmllint --format, and confirm the <g: namespace declaration is present on <rss>. If a caller has manually string-manipulated the output, the namespace can get lost and Google treats every attribute as invalid.

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