elliotjreed / google-shopping-feed
A PHP library for creating a Google Shopping feed (https://support.google.com/merchants/answer/7052112)
Package info
github.com/elliotjreed/google-shopping-feed
pkg:composer/elliotjreed/google-shopping-feed
Requires
- php: ^8.5
- ext-dom: *
- ext-xmlwriter: *
Requires (Dev)
- friendsofphp/php-cs-fixer: *
- phpunit/phpunit: ^13.2
- roave/security-advisories: dev-master
- squizlabs/php_codesniffer: *
README
google-shopping-feed
A PHP library for building Google Shopping product feeds and rendering them as RSS 2.0 XML.
It covers the full Google Merchant Center product data specification, including the newer conversational attributes, which can be rendered inline in your primary feed or submitted separately as a supplemental feed.
- Fluent setters for every attribute, with the attribute's own description on hover in your IDE.
- Validation as you set - a value Google would reject throws immediately, rather than failing when Google processes your feed hours later.
- Enums for every value set Google restricts, so invalid values cannot be spelled.
- Constant memory streaming for large catalogues. A 200,000 product feed producing 100MB of XML peaks at around 4MB of memory.
Contents
- Installation
- Quick start
- Rendering a feed
- Conversational attributes
- Grouped attributes
- Validation and exceptions
- Enumerations
- Attribute reference
- A complete example
- Development
Installation
composer require elliotjreed/google-shopping-feed
PHP 8.5 or above is required.
Quick start
<?php use ElliotJReed\GoogleShoppingFeed\Enum\Availability; use ElliotJReed\GoogleShoppingFeed\Enum\Condition; use ElliotJReed\GoogleShoppingFeed\Feed; use ElliotJReed\GoogleShoppingFeed\Product; $feed = new Feed('Example Shop', 'https://example.com', 'Everything we sell'); $feed->addProduct( (new Product()) ->setId('SKU-123') ->setTitle('Mens Organic Cotton T-Shirt') ->setDescription('A soft, breathable T-shirt made from 100% organic cotton.') ->setLink('https://example.com/products/sku-123') ->setImageLink('https://example.com/images/sku-123.jpg') ->setAvailability(Availability::InStock) ->setCondition(Condition::New) ->setPrice(24.99, 'GBP') ->setBrand('Example Brand') ->setGtin('3234567890126') ); echo $feed->toXml();
<?xml version="1.0" encoding="UTF-8"?> <rss xmlns:g="http://base.google.com/ns/1.0" version="2.0"> <channel> <title>Example Shop</title> <link>https://example.com</link> <description>Everything we sell</description> <item> <g:id>SKU-123</g:id> <g:title>Mens Organic Cotton T-Shirt</g:title> <g:description>A soft, breathable T-shirt made from 100% organic cotton.</g:description> <g:link>https://example.com/products/sku-123</g:link> <g:image_link>https://example.com/images/sku-123.jpg</g:image_link> <g:price>24.99 GBP</g:price> <g:availability>in_stock</g:availability> <g:brand>Example Brand</g:brand> <g:gtin>3234567890126</g:gtin> <g:condition>new</g:condition> </item> </channel> </rss>
On required attributes
Google requires the id, title, description, link, image_link, availability and price attributes for most
products, plus further attributes depending on the product's category and the country you sell to.
This library enforces only id, because every other requirement is conditional and enforcing them would reject
legitimate feeds. Consult the
product data specification for the requirements which apply to
your products.
On British English
Method names use British English, as does all documentation. Feed element names are Google's and are left exactly as Google defines them. The only place these differ is colour:
$product->setColour('Navy/White'); // renders as <g:color>Navy/White</g:color>
Rendering a feed
As a string
toXml() builds the whole feed in memory and returns it. It closes the feed, so no further products can be added
afterwards, but it may be called repeatedly and will return the same document each time.
$xml = $feed->toXml(); file_put_contents('feed.xml', $xml);
Streaming large catalogues
Feeds routinely contain hundreds of thousands of products. Pass a writable stream resource to stream(), add each
product as you read it from your database, then call end(). Every item is flushed to the stream as it is written, so
memory use stays constant no matter how large the catalogue is.
$handle = fopen('feed.xml', 'wb'); $feed = new Feed('Example Shop', 'https://example.com', 'Everything we sell'); $feed->stream($handle); foreach ($productRepository->all() as $row) { $feed->addProduct($this->mapToProduct($row)); } $feed->end(); fclose($handle);
Any writable stream works, so a feed can be sent straight to the browser without ever touching disk:
header('Content-Type: application/xml; charset=utf-8'); $feed = new Feed('Example Shop', 'https://example.com', 'Everything we sell'); $feed->stream(fopen('php://output', 'wb')); foreach ($productRepository->all() as $row) { $feed->addProduct($this->mapToProduct($row)); } $feed->end();
A streamed feed cannot also be returned as a string: calling toXml() on one throws InvalidFeedState. The stream is
left open for you to close.
Conversational attributes
The conversational attributes give AI-powered shopping
experiences the extra context needed to answer customer questions, connect related products and describe variants.
There are six: question_and_answer, document_link, related_product, item_group_title, variant_option and
popularity_rank.
You set them on the Product exactly like any other attribute:
use ElliotJReed\GoogleShoppingFeed\Attribute\QuestionAndAnswer; use ElliotJReed\GoogleShoppingFeed\Attribute\RelatedProduct; use ElliotJReed\GoogleShoppingFeed\Attribute\VariantOption; use ElliotJReed\GoogleShoppingFeed\Enum\IdentifierType; use ElliotJReed\GoogleShoppingFeed\Enum\RelationshipType; $product ->addQuestionAndAnswer(new QuestionAndAnswer('Is it machine washable?', 'Yes, at 30 degrees.')) ->addDocumentLink('https://example.com/guides/sizing.pdf') ->addRelatedProduct(new RelatedProduct(RelationshipType::Accessory, IdentifierType::Id, 'SKU-456')) ->setItemGroupTitle("Organic cotton men's T-shirt") ->addVariantOption(new VariantOption('Size', 'M')) ->setPopularityRank(95.5);
Where they are rendered is then up to the feed.
In your primary feed
Feed renders them inline by default, so the example above needs nothing further.
As a supplemental feed
Google recommends submitting the conversational attributes through a supplemental data source, because doing so cannot affect the approval status of your existing products.
SupplementalFeed takes the same Product objects and renders each one as an item containing its id and its
conversational attributes, and nothing else, so Google can match the supplemental data to products already in your
account:
use ElliotJReed\GoogleShoppingFeed\SupplementalFeed; $supplemental = new SupplementalFeed('Example Shop', 'https://example.com', 'Conversational attributes'); $supplemental->addProduct($product); echo $supplemental->toXml();
<?xml version="1.0" encoding="UTF-8"?> <rss xmlns:g="http://base.google.com/ns/1.0" version="2.0"> <channel> <title>Example Shop</title> <link>https://example.com</link> <description>Conversational attributes</description> <item> <g:id>SKU-123</g:id> <g:question_and_answer> <g:question>Is it machine washable?</g:question> <g:answer>Yes, at 30 degrees.</g:answer> </g:question_and_answer> <g:document_link>https://example.com/guides/sizing.pdf</g:document_link> <g:related_product> <g:relationship_type>accessory</g:relationship_type> <g:identifier_type>id</g:identifier_type> <g:identifier>SKU-456</g:identifier> </g:related_product> <g:item_group_title>Organic cotton men's T-shirt</g:item_group_title> <g:variant_option> <g:name>Size</g:name> <g:value>M</g:value> </g:variant_option> <g:popularity_rank>95.5</g:popularity_rank> </item> </channel> </rss>
Where you submit them separately, tell the primary feed to leave them out so the same data is not sent twice:
$feed = (new Feed('Example Shop', 'https://example.com', 'Everything we sell')) ->withoutConversationalAttributes();
SupplementalFeed extends Feed, so stream(), end() and toXml() all behave identically.
Grouped attributes
Attributes Google represents as a parent element containing sub-elements are built as objects, found in the
ElliotJReed\GoogleShoppingFeed\Attribute namespace. Required sub-attributes are constructor arguments; optional ones
have fluent setters.
use ElliotJReed\GoogleShoppingFeed\Attribute\Shipping; $product->addShipping( (new Shipping('GB')) ->setService('Standard') ->setPrice(3.99, 'GBP') ->setMinHandlingTime(1) ->setMaxHandlingTime(3) ->setMinTransitTime(2) ->setMaxTransitTime(5) );
<g:shipping> <g:country>GB</g:country> <g:service>Standard</g:service> <g:price>3.99 GBP</g:price> <g:min_handling_time>1</g:min_handling_time> <g:max_handling_time>3</g:max_handling_time> <g:min_transit_time>2</g:min_transit_time> <g:max_transit_time>5</g:max_transit_time> </g:shipping>
| Class | Feed attribute | Constructor |
|---|---|---|
Certification |
certification |
(string $authority, string $name, string $code) |
FreeShippingThreshold |
free_shipping_threshold |
(string $country, float|int|string $priceThreshold, string $currency) |
Installment |
installment |
(int $months, float|int|string $amount, string $currency) |
LoyaltyProgram |
loyalty_program |
() - every sub-attribute is optional |
ProductDetail |
product_detail |
(string $sectionName, string $attributeName, string $attributeValue) |
QuestionAndAnswer |
question_and_answer |
(string $question, string $answer) |
RelatedProduct |
related_product |
(RelationshipType|string $relationshipType, IdentifierType|string $identifierType, string $identifier) |
Shipping |
shipping |
(string $country) |
StructuredDescription |
structured_description |
(string $content, DigitalSourceType|string $digitalSourceType = Default) |
StructuredTitle |
structured_title |
(string $content, DigitalSourceType|string $digitalSourceType = Default) |
SubscriptionCost |
subscription_cost |
(SubscriptionPeriod|string $period, int $periodLength, float|int|string $amount, string $currency) |
Tax |
tax |
(string $country, float $rate) |
VariantOption |
variant_option |
(string $name, string $value) |
Validation and exceptions
Every setter validates its value immediately and throws
ElliotJReed\GoogleShoppingFeed\Exception\InvalidAttribute when Google would reject it:
$product->setTitle(str_repeat('a', 200)); // InvalidAttribute: The title attribute must not exceed 150 characters, 200 characters given. $product->setPrice(9.99, 'GBPX'); // InvalidAttribute: "GBPX" is not a valid ISO 4217 currency code. $product->setLink('example.com/product'); // InvalidAttribute: The link attribute must be a valid HTTP or HTTPS URL, "example.com/product" given. $product->setAvailability('sold_out'); // InvalidAttribute: "sold_out" is not a valid value for the availability attribute. // Allowed values: in_stock, out_of_stock, preorder, backorder.
What is checked:
- Character limits, measured in characters rather than bytes, so accented and non-Latin text is not truncated early.
- Repetition limits - for example
additional_image_linkup to 10 times,shippingup to 100,variant_optionup to 30. - Numeric ranges - for example
popularity_rankbetween 0 and 100, product dimensions between 1 and 3,000. - URLs must be well formed and use the
httporhttpsscheme. - Currencies are checked against the ISO 4217 code list, and countries against ISO 3166-1 alpha-2.
- Prices must use a full stop as the decimal separator and must not be negative. Numeric strings are preserved verbatim so prices stored as decimal strings do not lose precision through a float conversion.
- Empty values are rejected. Where you have no value for an attribute, do not call its setter.
- Characters which are invalid in XML are stripped, and text which is not valid UTF-8 is rejected. Both otherwise produce a feed Google cannot parse.
Misusing a feed rather than an attribute throws InvalidFeedState - for example calling toXml() on a feed which is
being streamed, or adding a product after the feed has ended.
Both exceptions implement ElliotJReed\GoogleShoppingFeed\Exception\GoogleShoppingFeedException, so everything this
library throws can be caught together:
use ElliotJReed\GoogleShoppingFeed\Exception\GoogleShoppingFeedException; try { $feed->addProduct($this->mapToProduct($row)); } catch (GoogleShoppingFeedException $exception) { $this->logger->warning('Skipped product', ['sku' => $row['sku'], 'reason' => $exception->getMessage()]); }
InvalidAttribute extends InvalidArgumentException and InvalidFeedState extends LogicException, so both can also
be caught as standard SPL exceptions.
Dates
Attributes taking a date accept a DateTimeInterface and are formatted to ISO 8601 for you, so a malformed date string
is not possible:
$product ->setAvailabilityDate(new DateTimeImmutable('2026-09-01 09:00:00')) ->setSalePriceEffectiveDate( new DateTimeImmutable('2026-08-01 00:00:00'), new DateTimeImmutable('2026-08-31 23:59:59') );
Enumerations
Every attribute whose values Google restricts has a backed enum in ElliotJReed\GoogleShoppingFeed\Enum. Setters
accept either the enum case or its string value, so you can pass a value straight from your database without mapping it
first - an unrecognised string throws:
$product->setAvailability(Availability::InStock); $product->setAvailability('in_stock'); // identical $product->setAvailability('sold_out'); // throws InvalidAttribute
| Enum | Used by | Cases |
|---|---|---|
AgeGroup |
age_group |
newborn, infant, toddler, kids, adult |
Availability |
availability |
in_stock, out_of_stock, preorder, backorder |
Condition |
condition |
new, refurbished, used |
CreditType |
installment |
finance, lease |
Destination |
included_destination, excluded_destination |
Shopping_ads, Display_ads, Local_inventory_ads, Free_listings, Free_local_listings, Cloud_retail, Local_cloud_retail, youtube_affiliate, youtube_merchandise |
DigitalSourceType |
structured_title, structured_description |
default, trained_algorithmic_media |
DimensionUnit |
product and delivery dimensions | cm, in |
EnergyEfficiencyClass |
the energy efficiency attributes | A+++ to G |
Gender |
gender |
male, female, unisex |
IdentifierType |
related_product |
gtin, id |
Pause |
pause |
ads, all |
RelationshipType |
related_product |
part_of_set, required_part, often_bought_with, substitute, different_brand, accessory |
SizeSystem |
size_system |
US, UK, EU, DE, FR, JP, CN, IT, BR, MEX, AU |
SizeType |
size_type |
regular, petite, maternity, big, tall, plus |
SubscriptionPeriod |
subscription_cost |
week, month, year |
SustainabilityIncentiveType |
sustainability_incentive_type |
ev_tax_credit, ev_price_discount |
UnitPricingUnit |
the unit pricing attributes | oz, lb, mg, g, kg, floz, pt, qt, gal, ml, cl, l, cbm, in, ft, yd, cm, m, sqft, sqm, ct, sheet, item |
WeightUnit |
product and delivery weights | lb, oz, g, kg |
YesNo |
the boolean attributes | yes, no |
Attributes Google expresses as yes or no take a PHP bool:
$product->setAdult(false)->setIsBundle(true)->setIdentifierExists(true);
Attribute reference
Methods beginning add may be called more than once; the attribute is repeated in the feed in the order values were
added.
Basic product data
| Attribute | Method |
|---|---|
id |
setId(string $id) |
title |
setTitle(string $title) |
structured_title |
setStructuredTitle(StructuredTitle $structuredTitle) |
description |
setDescription(string $description) |
structured_description |
setStructuredDescription(StructuredDescription $structuredDescription) |
link |
setLink(string $link) |
mobile_link |
setMobileLink(string $mobileLink) |
canonical_link |
setCanonicalLink(string $canonicalLink) |
link_template |
setLinkTemplate(string $linkTemplate) |
mobile_link_template |
setMobileLinkTemplate(string $mobileLinkTemplate) |
image_link |
setImageLink(string $imageLink) |
additional_image_link |
addAdditionalImageLink(string $additionalImageLink) |
lifestyle_image_link |
addLifestyleImageLink(string $lifestyleImageLink) |
video_link |
addVideoLink(string $videoLink) |
virtual_model_link |
setVirtualModelLink(string $virtualModelLink) |
Price and availability
| Attribute | Method |
|---|---|
price |
setPrice(float|int|string $amount, string $currency) |
sale_price |
setSalePrice(float|int|string $amount, string $currency) |
sale_price_effective_date |
setSalePriceEffectiveDate(DateTimeInterface $start, DateTimeInterface $end) |
unit_pricing_measure |
setUnitPricingMeasure(float $measure, UnitPricingUnit|string $unit) |
unit_pricing_base_measure |
setUnitPricingBaseMeasure(int $measure, UnitPricingUnit|string $unit) |
installment |
setInstallment(Installment $installment) |
subscription_cost |
setSubscriptionCost(SubscriptionCost $subscriptionCost) |
loyalty_program |
addLoyaltyProgram(LoyaltyProgram $loyaltyProgram) |
auto_pricing_min_price |
setAutoPricingMinPrice(float|int|string $amount, string $currency) |
maximum_retail_price |
setMaximumRetailPrice(float|int|string $amount, string $currency) |
cost_of_goods_sold |
setCostOfGoodsSold(float|int|string $amount, string $currency) |
availability |
setAvailability(Availability|string $availability) |
availability_date |
setAvailabilityDate(DateTimeInterface $availabilityDate) |
expiration_date |
setExpirationDate(DateTimeInterface $expirationDate) |
disclosure_date |
setDisclosureDate(DateTimeInterface $disclosureDate) |
Product category
| Attribute | Method |
|---|---|
google_product_category |
setGoogleProductCategory(string $googleProductCategory) |
product_type |
addProductType(string $productType) |
Product identifiers
| Attribute | Method |
|---|---|
brand |
setBrand(string $brand) |
gtin |
setGtin(string $gtin) |
mpn |
setMpn(string $mpn) |
identifier_exists |
setIdentifierExists(bool $identifierExists) |
Detailed product description
| Attribute | Method |
|---|---|
adult |
setAdult(bool $adult) |
age_group |
setAgeGroup(AgeGroup|string $ageGroup) |
is_bundle |
setIsBundle(bool $isBundle) |
certification |
addCertification(Certification $certification) |
gender |
setGender(Gender|string $gender) |
color |
setColour(string $colour) |
condition |
setCondition(Condition|string $condition) |
energy_efficiency_class |
setEnergyEfficiencyClass(EnergyEfficiencyClass|string $class) |
min_energy_efficiency_class |
setMinEnergyEfficiencyClass(EnergyEfficiencyClass|string $class) |
max_energy_efficiency_class |
setMaxEnergyEfficiencyClass(EnergyEfficiencyClass|string $class) |
item_group_id |
setItemGroupId(string $itemGroupId) |
material |
setMaterial(string $material) |
multipack |
setMultipack(int $multipack) |
pattern |
setPattern(string $pattern) |
product_detail |
addProductDetail(ProductDetail $productDetail) |
product_length |
setProductLength(float $length, DimensionUnit|string $unit) |
product_width |
setProductWidth(float $width, DimensionUnit|string $unit) |
product_height |
setProductHeight(float $height, DimensionUnit|string $unit) |
product_highlight |
addProductHighlight(string $productHighlight) |
product_weight |
setProductWeight(float $weight, WeightUnit|string $unit) |
size |
addSize(string $size) |
size_type |
setSizeType(SizeType|string $sizeType) |
size_system |
setSizeSystem(SizeSystem|string $sizeSystem) |
sustainability_incentive_type |
addSustainabilityIncentiveType(SustainabilityIncentiveType|string $type) |
Shopping campaigns and other configurations
| Attribute | Method |
|---|---|
ads_redirect |
setAdsRedirect(string $adsRedirect) |
custom_label_0 … custom_label_4 |
setCustomLabel(int $index, string $label) |
promotion_id |
addPromotionId(string $promotionId) |
Delivery and returns
| Attribute | Method |
|---|---|
shipping |
addShipping(Shipping $shipping) |
shipping_label |
setShippingLabel(string $shippingLabel) |
shipping_weight |
setShippingWeight(float $weight, WeightUnit|string $unit) |
shipping_length |
setShippingLength(float $length, DimensionUnit|string $unit) |
shipping_width |
setShippingWidth(float $width, DimensionUnit|string $unit) |
shipping_height |
setShippingHeight(float $height, DimensionUnit|string $unit) |
ships_from_country |
setShipsFromCountry(string $country) |
transit_time_label |
setTransitTimeLabel(string $transitTimeLabel) |
min_handling_time |
setMinHandlingTime(int $days) |
max_handling_time |
setMaxHandlingTime(int $days) |
free_shipping_threshold |
addFreeShippingThreshold(FreeShippingThreshold $threshold) |
return_policy_label |
setReturnPolicyLabel(string $returnPolicyLabel) |
tax |
addTax(Tax $tax) |
tax_category |
setTaxCategory(string $taxCategory) |
Destinations
| Attribute | Method |
|---|---|
included_destination |
addIncludedDestination(Destination|string $destination) |
excluded_destination |
addExcludedDestination(Destination|string $destination) |
shopping_ads_excluded_country |
addShoppingAdsExcludedCountry(string $country) |
Marketplaces
| Attribute | Method |
|---|---|
external_seller_id |
setExternalSellerId(string $externalSellerId) |
pause |
setPause(Pause|string $pause) |
Conversational attributes
| Attribute | Method |
|---|---|
question_and_answer |
addQuestionAndAnswer(QuestionAndAnswer $questionAndAnswer) |
document_link |
addDocumentLink(string $documentLink) |
related_product |
addRelatedProduct(RelatedProduct $relatedProduct) |
item_group_title |
setItemGroupTitle(string $itemGroupTitle) |
variant_option |
addVariantOption(VariantOption $variantOption) |
popularity_rank |
setPopularityRank(float $popularityRank) |
A complete example
<?php declare(strict_types=1); use ElliotJReed\GoogleShoppingFeed\Attribute\Certification; use ElliotJReed\GoogleShoppingFeed\Attribute\FreeShippingThreshold; use ElliotJReed\GoogleShoppingFeed\Attribute\Installment; use ElliotJReed\GoogleShoppingFeed\Attribute\LoyaltyProgram; use ElliotJReed\GoogleShoppingFeed\Attribute\ProductDetail; use ElliotJReed\GoogleShoppingFeed\Attribute\QuestionAndAnswer; use ElliotJReed\GoogleShoppingFeed\Attribute\RelatedProduct; use ElliotJReed\GoogleShoppingFeed\Attribute\Shipping; use ElliotJReed\GoogleShoppingFeed\Attribute\VariantOption; use ElliotJReed\GoogleShoppingFeed\Enum\AgeGroup; use ElliotJReed\GoogleShoppingFeed\Enum\Availability; use ElliotJReed\GoogleShoppingFeed\Enum\Condition; use ElliotJReed\GoogleShoppingFeed\Enum\DimensionUnit; use ElliotJReed\GoogleShoppingFeed\Enum\Gender; use ElliotJReed\GoogleShoppingFeed\Enum\IdentifierType; use ElliotJReed\GoogleShoppingFeed\Enum\RelationshipType; use ElliotJReed\GoogleShoppingFeed\Enum\SizeSystem; use ElliotJReed\GoogleShoppingFeed\Enum\SizeType; use ElliotJReed\GoogleShoppingFeed\Enum\WeightUnit; use ElliotJReed\GoogleShoppingFeed\Feed; use ElliotJReed\GoogleShoppingFeed\Product; $product = (new Product()) ->setId('SKU-123') ->setTitle('Mens Organic Cotton T-Shirt - Navy, Medium') ->setDescription('A soft, breathable T-shirt made from 100% GOTS-certified organic cotton.') ->setLink('https://example.com/products/sku-123') ->setImageLink('https://example.com/images/sku-123.jpg') ->addAdditionalImageLink('https://example.com/images/sku-123-back.jpg') ->addLifestyleImageLink('https://example.com/images/sku-123-worn.jpg') ->setPrice(24.99, 'GBP') ->setSalePrice(19.99, 'GBP') ->setSalePriceEffectiveDate( new DateTimeImmutable('2026-08-01 00:00:00'), new DateTimeImmutable('2026-08-31 23:59:59') ) ->setCostOfGoodsSold(8.50, 'GBP') ->setAvailability(Availability::InStock) ->setInstallment(new Installment(6, 4.16, 'GBP')) ->addLoyaltyProgram( (new LoyaltyProgram()) ->setProgramLabel('example_rewards') ->setTierLabel('gold') ->setPrice(17.99, 'GBP') ->setLoyaltyPoints(20) ) ->setGoogleProductCategory('Apparel & Accessories > Clothing > Shirts & Tops') ->addProductType('Home > Men > T-Shirts') ->setBrand('Example Brand') ->setGtin('3234567890126') ->setMpn('EX-TS-001-NVY-M') ->setCondition(Condition::New) ->setAgeGroup(AgeGroup::Adult) ->setGender(Gender::Male) ->setColour('Navy') ->setMaterial('cotton/elastane') ->addSize('M') ->setSizeType(SizeType::Regular) ->setSizeSystem(SizeSystem::UK) ->setItemGroupId('TS-001') ->addProductDetail(new ProductDetail('Fabric', 'Composition', '95% organic cotton, 5% elastane')) ->addProductHighlight('GOTS-certified organic cotton') ->addProductHighlight('Machine washable at 30 degrees') ->setProductWeight(0.25, WeightUnit::Kilograms) ->setProductLength(30, DimensionUnit::Centimetres) ->addCertification(new Certification('EC', 'EPREL', '123456')) ->setCustomLabel(0, 'spring_2026') ->setCustomLabel(1, 'best_seller') ->addPromotionId('SUMMER-SALE-2026') ->addShipping( (new Shipping('GB')) ->setService('Standard') ->setPrice(3.99, 'GBP') ->setMinHandlingTime(1) ->setMaxHandlingTime(2) ->setMinTransitTime(2) ->setMaxTransitTime(4) ) ->addFreeShippingThreshold(new FreeShippingThreshold('GB', 50, 'GBP')) ->setShipsFromCountry('GB') ->setShippingWeight(0.3, WeightUnit::Kilograms) ->setReturnPolicyLabel('extended_returns') ->addQuestionAndAnswer(new QuestionAndAnswer('Is it machine washable?', 'Yes, at 30 degrees.')) ->addQuestionAndAnswer(new QuestionAndAnswer('Does it shrink?', 'It is pre-shrunk and will not shrink.')) ->addDocumentLink('https://example.com/guides/sizing.pdf') ->addRelatedProduct(new RelatedProduct(RelationshipType::Accessory, IdentifierType::Id, 'SKU-456')) ->setItemGroupTitle("Organic cotton men's T-shirt") ->addVariantOption(new VariantOption('Colour', 'Navy')) ->addVariantOption(new VariantOption('Size', 'M')) ->setPopularityRank(95.5); $feed = new Feed('Example Shop', 'https://example.com', 'Everything we sell'); echo $feed->addProduct($product)->toXml();
Development
PHP 8.5 or above and Composer is expected to be installed.
Installing Composer
For instructions on how to install Composer visit getcomposer.org.
Installing
After cloning this repository, change into the newly created directory and run:
composer install
or if you have installed Composer locally in your current directory:
php composer.phar install
This will install all dependencies needed for the project.
Henceforth, the rest of this README will assume composer is installed globally (ie. if you are using composer.phar you will need to use composer.phar instead of composer in your terminal / command-line).
Running the Tests
Unit tests
Unit testing in this project is via PHPUnit.
All unit tests can be run by executing:
composer phpunit
Debugging
To have PHPUnit stop and report on the first failing test encountered, run:
composer phpunit:debug
Code formatting
A standard for code style can be important when working in teams, as it means that less time is spent by developers processing what they are reading (as everything will be consistent).
Code formatting is automated via PHP-CS-Fixer. PHP-CS-Fixer will not format line lengths which do form part of the PSR-2 coding standards so these will product warnings when checked by PHP Code Sniffer.
These can be run by executing:
composer phpcs
Running everything
All of the tests can be run by executing:
composer test
Outdated dependencies
Checking for outdated Composer dependencies can be performed by executing:
composer outdated
Validating Composer configuration
Checking that the composer.json is valid can be performed by executing:
composer validate --no-check-publish
Running via GNU Make
If GNU Make is installed, you can replace the above composer command prefixes with make.
All of the tests can be run by executing:
make test
Running the tests on a Continuous Integration platform (eg. Github Actions)
Specific output formats better suited to CI platforms are included as Composer scripts.
To output unit test coverage in text and Clover XML format (which can be used for services such as Coveralls):
composer phpunit:ci
To output PHP-CS-Fixer (dry run) and PHPCS results in checkstyle format (which GitHub Actions will use to output a readable format):
composer phpcs:ci
Github Actions
Look at the example in .github/workflows/main.yml.
Built With
License
This project is licensed under the MIT License - see the LICENCE.md file for details.