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.
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.
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:
SyliusBundleFixturesBundleFixtureFixtureInterface
and register this class as a service under the sylius_fixtures.fixture
tag in order to be used in suite configuration.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 😉
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.
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).
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.
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 😉 *
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).
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"
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();
}
}
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>
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.
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!