The future of Sylius API. Read more

Blog

Welcome to our blog, where we share news related to Sylius and post about technology & eCommerce.


29.01.2018 | 7 mins read

How-To: Manage Fixtures (Sample Data) in Your Project with SyliusFixturesBundle

Hello world! As somebody may recognize, I am the proud new member of Sylius Team and I am honored to be the author of the very first “How-to” blog posts here! With this article, we would like to reach further with our developers’ community and share more guided way to start working with Sylius and its components/bundles. Today, I am pleased to present SyliusFixturesBundle created by our lead Sylius Maintainer and Developer – Kamil Kokot.

What exactly are “fixtures”?

The concept of Fixtures is based on DoctrineFixturesBundle (but enhanced by us in our bundle). To give you a better look what can be achieved with Fixtures (and our bundle), let me present you a simple example.

Let’s say you develop an eCommerce site for your offline business. During the development, you would want to preview how the shop is going to look like when it’s done or test the applications’ performance with production-like amount of products/orders/anything you would possibly need. That’s where Fixtures come in! By using only one command in the CLI and a few classes or yml configuration files, you can generate sample data in you Application. It can greatly benefit in multiple areas of development like manual tests, design process or even business feedback.

SyliusFixturesBundle specifics

Now, when you are aware what problems fixtures can solve, I will present to you more details about our bundle. First, we need to distinguish three concepts:

  • Suite – just like in Behat scenarios, you can make custom suites which can contain the list of fixtures to be processed within its execution. You can create different suits for different development scenarios. For example: 10000 products in one category, 777 reviews of the product, 100 orders in last 7 days etc.
  • Fixture – is just a plain PHP object, that can change your system’s state. It can do various actions like persisting entities in the database, upload files, dispatch events or do anything you like. The only requirement is that you need to implement the SyliusBundleFixturesBundleFixtureFixtureInterface and register this class as a service under the sylius_fixtures.fixture tag in order to be used in suite configuration.
  • Listener – just like Fixture, it is a plain PHP object that allows you to execute code at some point of fixtures/suites execution. We have various events to listen on, you can read more about them here.
    We’ve got also some built-in Listeners that I will not cover in this blog post but I encourage you to learn about them. You can find more details about them here.

How do we use them?

Sylius Standard has a few fixtures out of the box. Before we move on to custom ones, I want to stop here for a second and explain how do we do it in Sylius way 😉

Sylius Standard Style!

If you checked our codebase (even briefly), you could have noticed a mysterious CoreBundle folder in the Sylius/Sylius repository. It is our main bundle where each of the independent bundles like ProductBundle, TaxonBundle, etc. is being bound with each other. That’s also a module where we put all of our fixtures for fresh Sylius Standard installation.

Products

There would be no shop without products so let’s take a closer look at SyliusBundleCoreBundleFixtureProductFixture. What is noticeable at first glance is that this class does not implement FixtureInterface as I recommended earlier, but extends AbstractResourceFixture. It is our custom made base class for some of the Fixtures that are created using ResourceBundle (which will be covered in the future blog post).

Factories

The content of the ProductFixture contains only two methods: getName() and configureResourceNode(). As you can see there is no method that actually creates a product, its variants… and its pricings… and it does not associate it with a taxon…. This process is rather complex. There are so much actions going on, we have separated this logic to custom SyliusBundleCoreBundleFixtureFactoryProductExampleFactory. Factories help us to keep our Fixtures clean and provide us a reusable code with yaml configuration. Sticking with best practices is one of our priorities – that’s why the ProductExampleFactory uses 6 other factories which gives us more reusable code.

Your custom fixtures

Let’s say that in our AcmeShop we want:
* 2 languages – English and Spanish.
* 10 example products

Of course, you can add this manually from the Admin panel, it is not that complicated and it probably won’t take so much time. But you can also do it like a Pro(grammer) with Fixtures! I’m going to focus on the second path and present you some code examples how to handle such requirements.

At First – we need to create our Fixture class. You can put it wherever you want, but we suggest the following structure:

src
└── AppBundle
    ├── AppBundle.php
    ├── DependencyInjection
    │   ├── AppExtension.php
    │   └── Configuration.php
    ├── Fixtures
    │   ├── AcmeProductFixture.php
    │   ├── Factory
    │   │   └── AcmeProductFactory.php
    │   └── Listener
    │       └── AcmeProductListener.php
    └── Resources
        └── config
            ├── app
            │   ├── config.yml
            │   └── fixtures
            │       ├── acme_product_suite.yml
            │       └── acme_suite.yml
            └── services.xml

*The tree structure above also covers files from the next steps of the tutorial. Little spoilers 😉 *

Hablas Español?

The first requirement of our shop is to set up two languages – English and Spanish. We will handle that using Fixture provided with Sylius Standard.

# src/AppBundle/Resources/config/app/fixtures/acme_suite.yml

sylius_fixtures:
    suites:
        acme_suite:
            fixtures:
                locale:
                    options:
                        locales: ['en_US', 'es_ES']

The only thing we need to set-up is a simple .yml file. That’s it. When you run the acme_suite, your shop will have two languages (and nothing more).

Creating products

The previous example was a simple operation using already prepared Fixture. But let’s assume that we want some custom Product logic that cannot be handled with Sylius Standard one. To do that, we will also use Factories. Let me show you how to implement them in a few easy steps and what cool features we can perform on them. First – let’s create a Factory class:

<?php

// src/AppBundle/Fixtures/Factory/AcmeProductFactory.php

declare(strict_types=1);

namespace AppBundle\Fixtures\Factory;

use Sylius\Bundle\CoreBundle\Fixture\Factory\AbstractExampleFactory;
use Sylius\Component\Core\Formatter\StringInflector;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Product\Generator\SlugGeneratorInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

class AcmeProductFactory extends AbstractExampleFactory
{
    /**
     * @var FactoryInterface
     */
    private $productFactory;

    /**
     * @var SlugGeneratorInterface
     */
    private $slugGenerator;

    /**
     * @var \Faker\Generator
     */
    private $faker;

    /**
     * @var OptionsResolver
     */
    private $optionsResolver;

    public function __construct(FactoryInterface $productFactory, SlugGeneratorInterface $slugGenerator)
    {
        $this->productFactory = $productFactory;
        $this->slugGenerator = $slugGenerator;

        $this->faker = \Faker\Factory::create();
        $this->optionsResolver = new OptionsResolver();

        $this->configureOptions($this->optionsResolver);
    }

    protected function configureOptions(OptionsResolver $resolver): void
    {
        $resolver
            ->setDefault('slug_prefix', function (Options $options): string {
                return 'acme';
            })
            ->setDefault('slug_version', function (Options $options): int {
                return 1;
            })
            ->setDefault('name', function (Options $options): string {
                return $this->faker->sentence;
            })
            ->setDefault('code', function (Options $options): string {
                return StringInflector::nameToCode($options['name']);
            })
        ;
    }

    public function create(array $options = []): ProductInterface
    {
        $options = $this->optionsResolver->resolve($options);

        /** @var ProductInterface $product */
        $product = $this->productFactory->createNew();
        $product->setCode($options['code']);
        $product->setName($options['name']);
        $product->setSlug(sprintf(
            '%s-%d-%s',
            $options['slug_prefix'],
            $options['slug_version'],
            $this->slugGenerator->generate($options['name'])
        ));

        return $product;
    }
}

I extend it with AbstractExampleFactory to help myself with some configuration. If you use our bundle separately from Sylius Standard, you can skip that.

One of the coolest feature of Factories is their configurability. In configureOptions() method, we can define the parameters for our factory that can be passed in suite configuration. You can take a look how to use those parameters in our Products Fixture implementation here: /tests/DataFixtures/ORM/resources/products.yml

Finally – The fixture using our custom Factory:

<?php

// src/AppBundle/Fixtures/AcmeWarehouseProductFixture.php

declare(strict_types=1);

namespace AppBundle\Fixtures;

use AppBundle\Fixtures\Factory\AcmeProductFactory;
use Doctrine\Common\Persistence\ObjectManager;
use Sylius\Bundle\FixturesBundle\Fixture\AbstractFixture;
use Sylius\Component\Core\Model\ProductInterface;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;

class AcmeProductFixture extends AbstractFixture
{
    /**
     * @var ObjectManager
     */
    private $productManager;

    /**
     * @var AcmeProductFactory
     */
    private $productFactory;

    public function __construct(ObjectManager $productManager, AcmeProductFactory $customProductFactory)
    {
        $this->productManager = $productManager;
        $this->productFactory = $customProductFactory;
    }

    public function load(array $options): void
    {
        for ($i = 0; $i < 10; $i++) {
            /** @var ProductInterface $product */
            $product = $this->productFactory->create();

            $this->productManager->persist($product);
        }

        $this->productManager->flush();
    }

    public function getName(): string
    {
        return 'acme_products';
    }

    protected function configureOptionsNode(ArrayNodeDefinition $optionsNode): void
    {
        $optionsNode
            ->children()
                ->scalarNode('slug_prefix')->cannotBeEmpty()->end()
                ->scalarNode('slug_version')->cannotBeEmpty()->end()
        ;
    }
}

We’ve got here a key method configureOptionsNode(), which is just like a DependencyInjection Configuration class in Symfony. It will handle the .yml parameters from suite configuration and pass them to our Factory. Now we can use it like in the configuration below:

# src/AppBundle/Resources/config/app/fixtures/acme_product_suite.yml

sylius_fixtures:
    suites:
        acme_product_suite:
            listeners:
                acme_product_listener: ~
            fixtures:
                acme_products:
                    options:
                        slug_prefix: "sylius"
                        slug_version: "10"

Listeners

There is one concept left that I haven’t covered yet. During Fixtures execution we dispatch some events that can be listened to. This is where Listeners come in. They can perform any action before or after suite/fixture execution. Just implement an interface suitable for you in your Listener:

SyliusBundleFixturesBundleListenerBeforeSuiteListenerInterface,
SyliusBundleFixturesBundleListenerAfterSuiteListenerInterface,
SyliusBundleFixturesBundleListenerBeforeFixtureListenerInterface,
SyliusBundleFixturesBundleListenerAfterFixtureListenerInterface 

More on that is described in our Documentation here.

In our Listener, we want to re-assign a slug for newly created Products. It means, that we need to use AfterFixtureListenerInterface and use Doctrine to save our Products again with new slugs.

<?php

// src/AppBundle/Fixtures/Listener/AcmeProductListener.php

declare(strict_types=1);

namespace AppBundle\Fixtures\Listener;

use Doctrine\Common\Persistence\ObjectManager;
use Sylius\Bundle\FixturesBundle\Listener\AbstractListener;
use Sylius\Bundle\FixturesBundle\Listener\AfterFixtureListenerInterface;
use Sylius\Bundle\FixturesBundle\Listener\FixtureEvent;
use Sylius\Component\Core\Model\Product;
use Sylius\Component\Core\Model\ProductInterface;

final class AcmeProductListener extends AbstractListener implements AfterFixtureListenerInterface
{
    /**
     * @var ObjectManager
     */
    private $productManager;

    public function __construct(ObjectManager $productManager)
    {
        $this->productManager = $productManager;
    }

    public function getName(): string
    {
        return 'acme_product_listener';
    }

    public function afterFixture(FixtureEvent $fixtureEvent, array $options): void
    {
        $products = $this->productManager->getRepository(Product::class)->findAll();

        /** @var ProductInterface $product */
        foreach ($products as $product) {
            $product->setSlug(sprintf(
                '%s-%s',
                $fixtureEvent->fixture()->getName(),
                $product->getSlug()
            ));

            $this->productManager->persist($product);
        }

        print(sprintf(
            "Successfully assigned slugs to %s fixture\n",
            $fixtureEvent->fixture()->getName()
        ));

        $this->productManager->flush();
    }
}

Service configuration

Because our Fixtures, Factories and Listeners work as a services, we can inject into them some other services or set a custom tags. The configuration below is all what is needed to run the above codes.

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

<!-- src/AppBundle/Resources/config/services.xml -->

<container
        xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
    <services>
        <service id="acme.fixture.products" class="AppBundle\Fixtures\AcmeProductFixture">
            <argument type="service" id="doctrine.orm.entity_manager" />
            <argument type="service" id="acme.factory.acme_product" />
            <tag name="sylius_fixtures.fixture" />
        </service>

        <service id="acme.factory.acme_product" class="AppBundle\Fixtures\Factory\AcmeProductFactory">
            <argument type="service" id="sylius.factory.product" />
            <argument type="service" id="sylius.generator.slug" />
        </service>

        <service id="acme.listener.acme_product" class="AppBundle\Fixtures\Listener\AcmeProductListener">
            <argument type="service" id="doctrine.orm.entity_manager" />
            <tag name="sylius_fixtures.listener" />
        </service>
    </services>
</container>

Final configuration

Now we can proceed to final step which is suite configuration. I will create a new fixtures.yml in src/AppBundle/Resources/config/app. Its content will look like this:

# src/AppBundle/Resources/config/app/config.yml

imports:
    - { resource: 'fixtures/acme_suite.yml' }
    - { resource: 'fixtures/acme_product_suite.yml' }

If you go to the command line and list all available suites and fixtures by bin/console sylius:fixture:list, you will see the following:

Running your custom suite of fixtures is also easy. Just use php bin/console sylius:fixtures:load acme_suite. It should ask you if you really want to erase current environment database and install fixtures.

Summary

I hope this article will help and encourage you to start a journey with our Bundles and Components. We can’t wait to share more of our knowledge in the upcoming posts so stay tuned and subscribe to our newsletter!

Getting started with Sylius
Online course (8h)

Be the first to find out about new posts. Join to our newsletter!