7 min. read
Categories: Technical
How-To: Manage Fixtures (Sample Data) in Your Project with SyliusFixturesBundle
We're starting a new series of "How-to" articles, aiming to create the best possible know-how for Sylius developers. Read more if you want to understand the power of SyliusFixturesBundle!
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!

Share:
More from our blog
Technical 7 min read 04.12.2024
Here’s everything you had to know about the first major release since 2017! Over 7 years after the first major release, on Nov 12, 2024, we have released Sylius 2.0.0. We had a great opportunity to announce it first at SyliusCon in Lyon, but now, as we are back to… Read More
7 min read 22.11.2024
The emotions start to settle after SyliusCon, and it’s time to reflect on this incredible milestone in our journey. Why a milestone? Because SyliusCon exceeded our expectations in every possible way. We broke attendance records and brought together the key figures of our community, numerous partners, freelancers, and simply all… Read More
Cloud 7 min read 17.06.2024
We are thrilled to announce that we just signed a strategic partnership with Platform.sh, and as a result, we are extending our offer with Sylius Cloud powered by Platform.sh. Platform.sh is a modern Platform-as-a-Service (PaaS) solution that allows businesses to leverage the cloud environment without losing access to the code… Read More
Comments