elliotjreed/google-shopping-feed

A PHP library for creating a Google Shopping feed (https://support.google.com/merchants/answer/7052112)

Maintainers

Package info

github.com/elliotjreed/google-shopping-feed

pkg:composer/elliotjreed/google-shopping-feed

Transparency log

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-07-26 15:10 UTC

This package is auto-updated.

Last update: 2026-07-26 15:23:22 UTC


README

Contributor Covenant

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

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_link up to 10 times, shipping up to 100, variant_option up to 30.
  • Numeric ranges - for example popularity_rank between 0 and 100, product dimensions between 1 and 3,000.
  • URLs must be well formed and use the http or https scheme.
  • 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_0custom_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.