Shopware custom CLI command to generate demo data on your entity

Shopware custom CLI command to generate demo data on your entity

Vien Nguyen's photo
Vien Nguyen
·May 21, 2022·

5 min read

Table of contents

  • Folder structure
  • Registering the command
  • Configuring the command
  • Styling the command
  • Creating the generator
  • Running the command
  • Complete classes and XML file

In this blog post, you will learn how to create a custom command in a Shopware 6 plugin and use it to generate the demo data.

Because Shopware 6 is based on the Symfony framework, it also has its Console available. So you can do most of the stuff you need to do while developing Shopware 6 plugins.

Folder structure

<pluginRoot>
└── src
    ├── Command
    │   └── DemodataCommand.php
    │
    ├── Generator
    │   └── ArticleGenerator.php
    │
    └── Resources
        └── config
            └── services.xml

Registering the command

To register a new command, just add it to your plugin's services.xml and specify the console.command tag:

<service id="Sas\BlogModule\Command\DemodataCommand">
    <argument type="service" id="Shopware\Core\Framework\Demodata\DemodataService"/>
    <tag name="console.command"/>
</service>

Configuring the command

Your command's class should extend from the Symfony\Component\Console\Command\Command class:

class DemodataCommand extends Command
{
    protected static $defaultName = '';

    protected function configure(): void
    {
        //
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        //
    }
}

Let’s start with the name of the command (the part after "bin/console"). It's the description shown when running "php bin/console list":

protected static $defaultName = 'article:demodata';

You can optionally define a description, help message and the input options and arguments by overriding the configure() method:

protected function configure(): void
{
    $this->addArgument('count', InputArgument::REQUIRED, 'The number of the articles.');
}

Put in the execute() method the code to create demo data: This method must return an integer number with the "exit status code" of the command. You can also use these constants to make code more readable:

  • Command::SUCCESS if there was no problem running the command.
  • Command::FAILURE if some error happened during the execution.
  • Command::INVALID indicates incorrect command usage: invalid options or missing arguments.
protected function execute(InputInterface $input, OutputInterface $output): int
{
    // ... put here the code to create articles

    return Command::SUCCESS;
}

Styling the command

Title

It displays the given string as the command title. This method is meant to be used only once in a given command, but nothing prevents you from using it repeatedly:

$io->title('Article Data Generator');

The console output should be:

Article Data Generator
======================

Table

It displays the given array of headers and rows as a compact table:

$io->table(
    ['Entity', 'Items', 'Time'],
    $demoContext->getTimings()
);

The console output should be:

---------------------- ------- --------------------
  Entity                 Items   Time
---------------------- ------- --------------------
  media                  1000    3.7
  article                1000    6.1
---------------------- ------- --------------------

Progress bar

When executing longer-running commands, it may be helpful to show progress information, which updates as your command runs:

Generating 1000 items for article
---------------------------------

1000/1000 [============================] 100%

! [NOTE] Took 6.1 seconds

Start progress bar: It displays a progress bar with several steps equal to the argument passed to the method (don't progress bar's length of the progress bar is unknown).

// displays a progress bar of unknown length
$context->getConsole()->progressStart();

// displays a 100-step length progress bar
$context->getConsole()->progressStart(100);

Advance progress bar: It makes the progress bar advance the given number of steps.

// advances the progress bar 1 step
$context->getConsole()->progressAdvance();

// advances the progress bar 10 steps
$context->getConsole()->progressAdvance(10);

Finish progress bar: It finishes the progress bar (filling up all the remaining steps when its length is known).

$context->getConsole()->progressFinish();

Creating the generator

To register a new generator, add it to your plugin's services.xml and specify the shopware.demodata_generator tag:

<service id="Sas\BlogModule\Generator\ArticleGenerator">
    <argument type="service" id="Shopware\Core\Framework\DataAbstractionLayer\Write\EntityWriter" />
    <argument type="service" id="Doctrine\DBAL\Connection" />
    <argument type="service" id="Sas\BlogModule\Content\Article\ArticleDefinition"/>
    <tag name="shopware.demodata_generator"/>
</service>

Now, we need our generator to know its definition class. This is done by overriding the method getDefinition()

public function getDefinition(): string
{
    return ArticleDefinition::class;
}

Put in the generate() method the code to generate demo data:

public function generate(int $numberOfItems, DemodataContext $context, array $options = []): void
{
    $writeContext = WriteContext::createFromContext($context->getContext());

    $payload = [
        // an array of articles
    ];

    $this->writer->upsert($this->articleDefinition, $payload, $writeContext);
}

Running the command

bin/console article:demodata 1000

Complete classes and XML file

DemodataCommand.php

use Sas\BlogModule\Content\Article\ArticleDefinition;
use Shopware\Core\Framework\Adapter\Console\ShopwareStyle;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Demodata\DemodataRequest;
use Shopware\Core\Framework\Demodata\DemodataService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class DemodataCommand extends Command
{
    protected static $defaultName = 'article:demodata';

    private DemodataService $demodataService;

    public function __construct(DemodataService $demodataService)
    {
        parent::__construct();

        $this->demodataService = $demodataService;
    }

    protected function configure(): void
    {
        $this->addArgument('count', InputArgument::REQUIRED, 'The number of the articles.');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new ShopwareStyle($input, $output);
        $io->title('Article Data Generator');

        $context = Context::createDefaultContext();

        $request = new DemodataRequest();

        $request->add(ArticleDefinition::class, (int)$input->getArgument('count'));

        $demoContext = $this->demodataService->generate($request, $context, $io);

        $io->table(
            ['Entity', 'Items', 'Time'],
            $demoContext->getTimings()
        );

        return self::SUCCESS;
    }
}

ArticleGenerator.php

use Doctrine\DBAL\Connection;
use Sas\BlogModule\Content\Article\ArticleDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityWriterInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteContext;
use Shopware\Core\Framework\Demodata\DemodataContext;
use Shopware\Core\Framework\Demodata\DemodataGeneratorInterface;
use Shopware\Core\Framework\Uuid\Uuid;

class ArticleGenerator implements DemodataGeneratorInterface
{
    private EntityWriterInterface $writer;

    private Connection $connection;

    private ArticleDefinition $articleDefinition;

    public function __construct(
        EntityWriterInterface $writer,
        Connection            $connection,
        ArticleDefinition     $articleDefinition
    ) {
        $this->writer = $writer;
        $this->connection = $connection;
        $this->articleDefinition = $articleDefinition;
    }

    public function getDefinition(): string
    {
        return ArticleDefinition::class;
    }

    public function generate(int $numberOfItems, DemodataContext $context, array $options = []): void
    {
        $authorId = $this->connection->fetchOne('SELECT LOWER(HEX(id)) FROM author');

        $writeContext = WriteContext::createFromContext($context->getContext());

        $context->getConsole()->progressStart($numberOfItems);

        $payload = [];
        for ($i = 0; $i < $numberOfItems; ++$i) {
                        $translations = [
                'en-GB' => [
                    'title' => $title,
                    'teaser' => $context->getFaker()->text(200),
                    'content' => $context->getFaker()->text(50),
                ],
            ];

            $article = [
                'id' => Uuid::randomHex(),
                'active' => true,
                'authorId' => $authorId,
                'translations' => $translations
            ];

            $payload[] = $article;

            if (\count($payload) >= 100) {
                $this->writer->upsert($this->articleDefinition, $payload, $writeContext);
                $context->getConsole()->progressAdvance(\count($payload));
                $payload = [];
            }
        }

        if (!empty($payload)) {
            $this->writer->upsert($this->articleDefinition, $payload, $writeContext);
            $context->getConsole()->progressAdvance(\count($payload));
        }

        $context->getConsole()->progressFinish();
    }
}

services.xml

<?xml version="1.0" ?>

<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="Sas\BlogModule\Command\DemodataCommand">
            <argument type="service" id="Shopware\Core\Framework\Demodata\DemodataService"/>
            <tag name="console.command"/>
        </service>

        <service id="Sas\BlogModule\Generator\ArticleGenerator">
            <argument type="service" id="Shopware\Core\Framework\DataAbstractionLayer\Write\EntityWriter" />
            <argument type="service" id="Doctrine\DBAL\Connection" />
            <argument type="service" id="Sas\BlogModule\Content\Article\ArticleDefinition"/>
            <tag name="shopware.demodata_generator"/>
        </service>

    </services>
</container>

I am really happy to receive your feedback on this article. Thanks for your precious time reading this.

References: