Sylius Product Samples Plugin

Documentation

Version Not Yet Released

You are viewing the documentation for the 1.x branch of the Sylius Product Samples Plugin package which has not yet been released. Be aware that the API for this version may change before release.

Installation & Setup

To install this plugin, run the following Composer command:

composer require babdev/sylius-product-samples-plugin

The plugin adds fields to the channel, product, and product variant models, so installing it is not only a matter of registering the bundle. The steps below cover a complete working install.

Register The Plugin

For an application using Symfony Flex the plugin should be automatically registered, but if not you will need to add it to your config/bundles.php file.

<?php

return [
    // ...

    BabDev\SyliusProductSamplesPlugin\BabDevSyliusProductSamplesPlugin::class => ['all' => true],
];

Import the Configuration

In your config/packages/_sylius.yaml file, import the plugin's application configuration to automatically configure its integrations with other bundles.

imports:
    - { resource: "@BabDevSyliusProductSamplesPlugin/Resources/config/app/config.yaml" }

This wires the product samples fields into the admin channel form. The storefront integrations are opt-in and are covered further down.

Extend the Models

The plugin needs three extra pieces of data:

ModelAddition
ChannelmaxSamplesPerOrder, the per-order sample limit for that channel
ProductsamplesActive, whether the product offers samples
ProductVariantsample / sampleOf, the one-to-one link between a variant and its sample

The channel side is offered as an interface and a trait so your application is not forced into an inheritance chain it may already be using. The product and variant sides are concrete classes to extend.

Create the three classes in your application:

<?php

declare(strict_types=1);

namespace App\Entity\Channel;

use BabDev\SyliusProductSamplesPlugin\Model\ChannelInterface as ProductSamplesChannelInterface;
use BabDev\SyliusProductSamplesPlugin\Model\ChannelTrait as ProductSamplesChannelTrait;
use Sylius\Component\Core\Model\Channel as BaseChannel;

class Channel extends BaseChannel implements ProductSamplesChannelInterface
{
    use ProductSamplesChannelTrait;
}
<?php

declare(strict_types=1);

namespace App\Entity\Product;

use BabDev\SyliusProductSamplesPlugin\Model\Product as BaseProduct;

class Product extends BaseProduct
{
}
<?php

declare(strict_types=1);

namespace App\Entity\Product;

use BabDev\SyliusProductSamplesPlugin\Model\ProductVariant as BaseProductVariant;

class ProductVariant extends BaseProductVariant
{
}

If your application already extends Sylius' Product and ProductVariant, change the class each one extends to the plugin's; the plugin's classes extend the Sylius ones.

Then point the resource configuration at them in config/packages/_sylius.yaml:

sylius_channel:
    resources:
        channel:
            classes:
                model: 'App\Entity\Channel\Channel'

sylius_product:
    resources:
        product:
            classes:
                model: 'App\Entity\Product\Product'
        product_variant:
            classes:
                model: 'App\Entity\Product\ProductVariant'
                repository: 'BabDev\SyliusProductSamplesPlugin\Doctrine\ORM\ProductVariantRepository'
The repository is not optional. Sample variants are rows in the sylius_product_variant database table, so without the plugin's repository they show up alongside real variants in the admin product variant grid. The plugin's repository extends Sylius' own and adds a single sampleOf IS NULL condition to the grid queries. If your application already replaces the product variant repository, extend the plugin's class instead of Sylius'.

Map the New Fields in Doctrine

The plugin ships no Doctrine mapping of its own, because the fields have to be mapped onto your classes. Add the three mapping files below to your application's mapping directory, and register that directory if it is not already:

# config/packages/doctrine.yaml
doctrine:
    orm:
        mappings:
            App:
                type: xml
                dir: '%kernel.project_dir%/config/doctrine'
                prefix: App\Entity

config/doctrine/Channel.orm.xml:

<?xml version="1.0" encoding="UTF-8"?>

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
                                      http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

    <mapped-superclass name="App\Entity\Channel\Channel" table="sylius_channel">

        <field name="maxSamplesPerOrder" column="max_samples_per_order" type="integer" nullable="true" />

    </mapped-superclass>

</doctrine-mapping>

config/doctrine/Product.orm.xml:

<?xml version="1.0" encoding="UTF-8"?>

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
                                      http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

    <mapped-superclass name="App\Entity\Product\Product" table="sylius_product">

        <field name="samplesActive" column="samples_active" type="boolean" />

    </mapped-superclass>

</doctrine-mapping>

config/doctrine/ProductVariant.orm.xml:

<?xml version="1.0" encoding="UTF-8"?>

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
                                      http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

    <mapped-superclass name="App\Entity\Product\ProductVariant" table="sylius_product_variant">

        <one-to-one field="sample" target-entity="Sylius\Component\Product\Model\ProductVariantInterface" mapped-by="sampleOf">
            <cascade>
                <cascade-all />
            </cascade>
        </one-to-one>

        <one-to-one field="sampleOf" target-entity="Sylius\Component\Product\Model\ProductVariantInterface" inversed-by="sample">
            <cascade>
                <cascade-persist />
            </cascade>
            <join-column name="sample_of_id" on-delete="SET NULL" />
        </one-to-one>

    </mapped-superclass>

</doctrine-mapping>

Once the mappings are in place, update your schema:

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate

Wire Up the Storefront

Everything above is enough for the admin. The storefront needs the blocks and template overrides below to make samples available to customers.

Template Events

Add to config/packages/_sylius.yaml:

sylius_ui:
    events:
        sylius.shop.cart.widget.popup:
            blocks:
                content:
                    enabled: false
                sample_aware_content:
                    template: "@BabDevSyliusProductSamplesPlugin/Shop/Cart/Widget/_popup.html.twig"
                    priority: 10

        sylius.shop.checkout.sidebar:
            blocks:
                summary:
                    enabled: false
                sample_aware_summary:
                    template: "@BabDevSyliusProductSamplesPlugin/Shop/Checkout/_summary.html.twig"
                    priority: 20

        sylius.shop.layout.javascripts:
            blocks:
                request_a_sample_javascript:
                    template: 'JavaScript/plugin.html.twig'
                    priority: 0

The two enabled: false entries disable Sylius' own blocks; the plugin's replacements label sample line items as samples rather than repeating the product name.

Template Overrides

Several core templates expose neither a Twig block nor a sylius_template_event at the point the plugin needs to change, so the plugin carries forks of them. Each fork opens with a header naming its upstream path, the Sylius version it was taken from, and its one intended difference — diff them against upstream when you upgrade Sylius.

Override each one from your application with a one-line include:

{# templates/bundles/SyliusShopBundle/Product/Show/_addToCart.html.twig #}
{% include '@BabDevSyliusProductSamplesPlugin/Shop/Product/Show/_addToCart.html.twig' %}
{# templates/bundles/SyliusShopBundle/Product/Show/_variants.html.twig #}
{% include '@BabDevSyliusProductSamplesPlugin/Shop/Product/Show/_variants.html.twig' %}
{# templates/bundles/SyliusShopBundle/Product/Show/_variantsPricing.html.twig #}
{% include '@BabDevSyliusProductSamplesPlugin/Shop/Product/Show/_variantsPricing.html.twig' with {'pricing': pricing, 'variants': variants} %}
{# templates/bundles/SyliusShopBundle/Product/_info.html.twig #}
{% include '@BabDevSyliusProductSamplesPlugin/Shop/Product/_info.html.twig' %}
{# templates/bundles/SyliusAdminBundle/Product/_info.html.twig #}
{% include '@BabDevSyliusProductSamplesPlugin/Admin/Product/_info.html.twig' %}

What each one contributes:

OverrideEffect
Shop/Product/Show/_addToCartAdds the "request a sample" button next to "add to cart"
Shop/Product/Show/_variantsEmits the sample price data attributes the shop script reads
Shop/Product/Show/_variantsPricingFormats the sample price in the variant pricing map for products with options
Shop/Product/_infoAdds a sample badge to line items in the cart and order summaries
Admin/Product/_infoAdds a sample badge to line items in the admin order view

If you have already forked any of these templates for your own reasons, merge the plugin's customizations into your template rather than replacing it.

Shop JavaScript

The plugin ships a JavaScript component that swaps the sample button's label and price as the shopper selects a variant. Publish the bundle assets:

bin/console assets:install public

Then add the template referenced by the sylius.shop.layout.javascripts block above:

{# templates/JavaScript/plugin.html.twig #}
{% if app.request.attributes.get('_route') == 'sylius_shop_product_show' %}
    {% include '@SyliusUi/_javascripts.html.twig' with {'path': 'bundles/babdevsyliusproductsamplesplugin/babdev-product-samples-shop.js'} %}
{% endif %}

The route guard keeps the script off every other page; it is only useful on the product page. If you build your storefront assets with Webpack Encore, you can instead import the plugin's source from src/Resources/assets/shop/ into your own entry point.