2022-07-21 Adding the StorableFlow
instead of the FlowEvent
for implementing the flow DelayAction in Flow Builder
INFO
This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository. You can find the original version here
Context
The actions in Flow Builder are listening for Business Events. We want to implement the flow DelayAction in Flow Builder, that means the actions can be delayed, and will be executed after a set amount of time. But we have some problems after the action was delayed:
- Events may contain old data, that data may be updated during the delay, and currently we don't have any way to restore the data.
- After a delay the rules have to be re-evaluated, but the data in the rules could be outdated or changed, so the rules have to be reloaded as well. Or the rules do not exist anymore.
Decision
We would need to detach the Event System and the Flow System from each other, thus removing the dependency on the runtime objects within an event. Meaning the Actions must not have access to the original Events.
We would create a class StorableFlow
, that can store the data in the original event as scalar values, and restore the original data based on this stored data.
class StorableFlow
{
// contains the scalar values based on the original events
// $store can be serialized and used to restore the object data
protected array $store = [];
// contains the restored object data like the data we defined in the `availableData` in original events
// $data can not be serialized, but can be restored from $store
protected array $data = [];
public function __construct(array $store = [], array $data = [])
{
$this->store = $store;
$this->data = $data;
}
// This method will be called in each `Storer` to store the representation of data
public function setStore(string $key, $value) {
$this->store[$key] = $value;
}
public function getStore(string $key) {
return $this->store[$key];
}
// After we restored the data in `Storer`, we can set the data, we'll use `$this->data` instead getter data on original event
public function setData(string $key, $value) {
$this->data[$key] = $value;
}
public function getData(string $key) {
return $this->data[$key];
}
}
The StorableFlow
class will be use on Flow Builder:
Before:
class FlowDispatcher
{
public function dispatch(Event $event) {
...
// Currently, dispatch on Flow Builder use the original event to execute the Flow
$this->callFlowExecutor($event);
...
}
}
After:
class FlowDispatcher
{
public function dispatch(Event $event) {
...
// The `FlowFactory` will create/restore the `StorableFlow` from original event
$flow = $this->flowFactory->create($event);
// use the `StorableFlow` to execute the flow builder actions instead of the original events
$this->execute($flow);
...
}
}
- Flow Builder actions may no longer access the original event.
- Each Aware Interface gets its own
Storer
class to restore the data of Aware, so we have manyStorer
likeOrderStorer
,MailStorer
,CustomerStorer
... - The main task of a
Storer
is to restore the data from a scalar storage. - The
Storer
provides a store function, in order to store itself the data, in order restore the object - The
Storer
provides a restore function to restore the object using the store data.
interface FlowStorer {}
Example for OrderStorer
:
class OrderStorer implements FlowStorer
{
// This function to check the original event is the instanceof Aware interface, and store the representation.
public function store(FlowEventAware $event, array $storedData): array
{
if ($event instanceof OrderAware) {
$storedData['orderId'] = $event->getOrderId();
}
return $storedData;
}
// This function is restore the data based on representation in `storedData`
public function restore(StorableFlow $flow): void
{
if ($flow->hasStore('orderId')) {
// allows to provide a closure for lazy data loading
// this opens the opportunity to have lazy loading for big data
// When we load the entity, we need to add the necessary associations for each entity
$flow->lazy('order', [$this, 'load']);
}
...
}
}
About the additional data defined in availabelData
in original events, that aren't defined in any Aware Interfaces and we can't restore that data in the Storer
. To cover the additional data from original events, we will have another store
AdditionalStorer
to store those data.
class AdditionalStorer extends FlowStorer
{
public function store(FlowEventAware $event, array $storedData)
{
...
// based on the `getAvailableData` in the original event to get the type of additional data
$additionalDataTypes = $event::getAvailableData()->toArray();
foreach ($additionalDataTypes as $key => $eventData) {
// Check if the type of data is Entity or EntityCollection
// in the $storedData, we only store the presentation like ['id' => id, 'entity' => entity], we'll restore the data in `AdditionalStorer::restore`
if ($eventData['type'] === 'Entity' || 'EntityCollection') {
$storedData[$key] = [
'id' => $event->getId(),
'entity' => Entity
];
}
// Check if the type of data is ScalarValueType
if ($eventData['type'] === ScalarValueType) {
$storedData[$key] = value
}
// start to implement /Serializable for ObjectType
if ($eventData['type'] === ObjectType) {
$storedData[$key] = value->serialize()
}
...
}
...
return $storedData;
}
// this function make sure we can restore the additional data from original data are not covered in `Storer`
// The additional data can be other entity, because the entities we defined in Aware interface like `order`, `customer` ... covered be `Storer`
public function restore(StorableFlow $flow): void
{
if (type === entity) {
// About the associations for entity data, mostly the additional entity data is the base entity, we don't need to add associations for this
$flow->setData($key, $this->load());
} else {
$flow->setData($key, $flow->getStore($key));
}
...
}
}
About the associations for entity data, mostly the additional entity data is the base entity, we don't need to add associations for this. About the ObjectType
data, we enforce all values used in ObjectType implement /Serializable, and serialize the object before store to $storedData
.
- Flow Builder actions only work with the
StorableFlow
instead of theFlowEvent
. TheStorableFlow
will restore the data from original events viaStorer
, and the Actions can get the data viagetData($key)
fromStorableFlow
instead ofgetAvailableData
from original events.
Before, in the flow actions still dependency Aware interfaces:
public function handle(StorableFlow $event) {
...
$baseEvent = $event->getEvent();
if ($baseEvent instanceof CustomerAware) {
$customerId= $baseEvent->getCustomerId();
}
...
}
After in the flow actions:
public function handle(StorableFlow $event) {
...
if ($event->hasStore('customerId') {
$customerId= $event->getStore('customerId');
}
...
}
getAvailableData
must NOT be responsible for the access of the data.- To create new or restore the
StorableFlow
by on the existing stored data, we need to provider theFlowFactory
.
class FlowFactory
{
...
public function create(FlowEventAware $event)
{
$storedData = [];
foreach ($this->storer as $storer) {
// Storer are responsible to move the corresponding
// data from the original event
$storer->store($event, $storedData);
}
return $this->restore($storedData);
}
public function restore(array $stored = [], array $data = [])
{
$flow = new StorableFlow($stored, $data);
foreach ($this->storer as $storer) {
$storer->restore($flow);
}
return $flow;
}
...
}
But when executing a delayed actions, we won't have a StorableFlow
, we just have the $stored
from the previously stored StorableFlow
, and based on the $stored
, we can restore a new StorableFlow
.
Example in Delay Actions:
// In handler delay actions -> put the actions to `queue`
$stored = json_encode($flow->stored());
$connection->executeStatement('INSERT INTO `swag_delay_action` (store) VALUES (:stored)...', ['stored' => $stored]);
// In handler execute delay actions
$stored = 'SELECT store FROM `swag_delay_action` .... ';
$flow = $this->flowFactory->restore(json_decode($stored));
Consequences
Because we use the new class StorableFlow
instead of the FlowEvent
class in the Flow Builder, we cannot use the original events or aware interfaces anymore, but about the symfony event was listeners the FlowEvent
, those can continue to use the interfaces as the store is not yet filled during we'll remove it in next major version.
- In symfony event listeners: only use the interfaces as the store is not yet filled
- In the flow builder: Only use the store functionality as the interfaces might not be implemented