Last summer we started working on a new project that involves the e-commerce framework Sylius. As you might know, it makes use of state machines. A lot. This is fine as it’s easy to hook into them and that’s also what we did.
- Author
- Michael Zangerle
- Date
- August 3, 2022
- Reading time
- 2 Minutes
In this project, there is also an ERP system involved that needs to be informed of e.g. new orders. When a new order gets created in Sylius we also want to push this information to the ERP system. We created a state machine listener and in the push
method we call our custom ErpOderManager::push
logic which then does all the necessary things to forward the order to the ERP system.
<?php
declare(strict_types=1);
namespace App\Sync\StateMachineListener;
// ...
final class PushOrderOnCreate
{
private ErpOrderManager $erpOrderManager;
public function __construct(ErpOrderManager $erpOrderManager)
{
$this->erpOrderManager = $erpOrderManager;
}
public function push(OrderInterface $order): void
{
$this->erpOrderManager->push($order);
}
}
Register it as a service (in the service.yaml
) and let the statemachine know about it:
winzou_state_machine:
sylius_order:
callbacks:
after:
push_to_erp:
on: "create"
do: [ "@App\\Sync\\StateMachineListener\\PushOrderOnCreate", "push" ]
args: [ "object" ]
Perfect! This works like a charm, is easy to understand and consists of very little code.
But …
… unfortunately, this logic gets also executed when the fixtures get loaded. That’s something we don’t want. We want to differentiate between these two cases. Now we could have started fiddling around with environment variables, created a few helper scripts, enable all devs to connect to the review apps / testing environments; but there were quite a few “what ifs” coming up and just didn’t feel like a good thing to do (error-prone, complicated, cumbersome).
Service decoration
It turns out that the command to load fixtures in Sylius sylius:fixtures:load
is a service and that’s a perfect use-case for the service decorator pattern in Symfony, as we want to execute something before and after calling it.
Therefore we created a new command to load fixtures, which just calls the original command. But before that, we can now check if the communication to the ERP is enabled, if so disable it and re-enable it after loading the fixtures with the normal Sylius command. By using the same name for the command nothing changed in the way normal Sylius applications load fixtures as well.
<?php
declare(strict_types=1);
namespace App\Command;
use Sylius\Bundle\FixturesBundle\Command\FixturesLoadCommand;
// ...
final class SyliusFixtureDecoratorCommand extends Command
{
protected static $defaultName = 'sylius:fixtures:load';
// ...
public function __construct(FixturesLoadCommand $command, ErpClientInterface $erpClient)
{
parent::__construct();
$this->command = $command;
$this->erpClient = $erpClient;
}
protected function configure(): void
{
$this
->setName('sylius:fixtures:load')
->setDescription('Loads fixtures from given suite')
->addArgument('suite', InputArgument::OPTIONAL, 'Suite name', 'default');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$erpCommunicationStatus = $this->erpClient->isErpCommunicationEnabled();
$this->erpClient->disableErpCommunication();
$this->command->setApplication($this->getApplication());
$exitCode = $this->command->execute($input, $output);
if (true === $erpCommunicationStatus) {
$this->erpClient->enableErpCommunication();
}
return $exitCode;
}
}
Now the only thing that remains to be done is to add the decoration configuration in the service.yaml.
App\Command\SyliusFixtureDecoratorCommand:
decorates: Sylius\Bundle\FixturesBundle\Command\FixturesLoadCommand
arguments:
- '@.inner'
- '@App\Sync\Erp\ErpClientInterface'
That’s all there is to it! If you have any questions or would like to discuss details, please type us a message.