Flow Builder - How to create a flow with migration

Flow Builder - How to create a flow with migration

Let's say you're building a plugin and, want to support flow builder by creating some default flows which would appear on the flow list after installing your plugin.

Inserting your custom flow by migration is the only way to do it, let's assume I need a default flow that looks like this:

image.png

Let's find it out by following the steps below:

Creating a migration by using the command below:

bin/console database:create-migration -p YourPluginName --name AddLoveNewOrdersFlow

A new migration is generated, it should be placed in src/Migration folder

<pluginRoot>
└── src
    └── Migration
        └── Migration1654243773AddLoveNewOrdersFlow.php

This is the file content

<?php declare(strict_types=1);

namespace Example\Migration;

use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;

class Migration1654243773AddLoveNewOrdersFlow extends MigrationStep
{
    public function getCreationTimestamp(): int
    {
        return 1654243773;
    }

    public function update(Connection $connection): void
    {
        // implement update
    }

    public function updateDestructive(Connection $connection): void
    {
        // implement update destructive
    }
}

Great, let's go to the next step.

Configuring the migration

Data structure

There are two tables that will be responsible for storing data of flow (flow and flow_sequence). The relation is 1 flow to n flow_sequence

Flow
TypeField nameDescription
BINARY(16)idFlow id
VARCHAR(255)nameFlow name
MEDIUMTEXTdescriptionFlow description
VARCHAR(255)event_nameFlow event name, this is the list events and actions
INT(11)priorityThe priority is the execution order of flows with the same event_name
LONGBLOBpayloadThe hierarchy tree of the flow, it will be built automatically after inserting or updating the flow
TINYINT(1)invalidTo show the payload is correct or not
TINYINT(1)activeEnable or disable the flow
Flow Sequences
TypeField nameReferencesDescription
BINARY(16)idSequence id
BINARY(16)flow_idflow.idFlow id
BINARY(16)parent_idflow_sequence.idSequence id of parent sequence
BINARY(16)rule_idrule.idRule id for IF condition
VARCHAR(255)action_nameAction name, this is the list events and actions
JSONconfigThe config data of the action sequence
INT(11)positionDisplay position of sequences that have the same parent_id
INT(11)display_groupTo determine which group this sequence should be displayed in
TINYINT(1)true_caseTo determine this sequence belongs to true case or false case of the parent sequence
Data mapping

image.png

image.png

Write to the update() function

Firstly, we will insert a record to flow table

public function update(Connection $connection): void
{
    $flowId = Uuid::randomBytes();
    // insert flow
    $connection->insert(
        FlowDefinition::ENTITY_NAME,
        [
            'id' => $flowId,
            'name' => 'Love new orders',
            // check the list of event and reference actions on the link below
            // https://developer.shopware.com/docs/resources/references/core-reference/flow-reference
            'event_name' => 'checkout.order.placed',
            'description' => 'Love new orders',
            'priority' => 100,
            'active' => true,
            'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
        ]
    );
}

Then insert the list of sequences with flowId above

// insert flow sequences
/* ----- Start insert sequences have display_group = 1 -------*/
$sequenceConditionId = Uuid::randomBytes();
$connection->insert(
    FlowSequenceDefinition::ENTITY_NAME,
    [
        'id' => $sequenceConditionId,
        'flow_id' => $flowId,
        'parent_id' => null,
        // let's say you already have the rule id for this
        // the sequence has rule_id is not null and action_name is null means it is an If condition
        'rule_id' => Uuid::fromHexToBytes('b482bf23d5c74df0bf40723181713aa2'),
        'position' => 1,
        'display_group' => 1,
        'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
    ]
);

// the condition's true case sequences
$connection->insert(
    FlowSequenceDefinition::ENTITY_NAME,
    [
        'id' => Uuid::randomBytes(),
        'flow_id' => $flowId,
        'parent_id' => $sequenceConditionId,
        // the sequence has action_name is not null and rule_id is null means it is an Action
        // check the list of event and reference actions on the link below
        // https://developer.shopware.com/docs/resources/references/core-reference/flow-reference
        'action_name' => 'action.add.order.tag',
        'config' => \json_encode([
            'entity' => 'order',
            'tagIds' => ['4b95ee7df46c429fa629f29e485cd626' => 'Love it'],
        ]),
        'position' => 1,
        'display_group' => 1,
        'true_case' => 1, // true
        'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
    ]
);

$connection->insert(
    FlowSequenceDefinition::ENTITY_NAME,
    [
        'id' => Uuid::randomBytes(),
        'flow_id' => $flowId,
        'parent_id' => $sequenceConditionId,
        // the sequence has action_name is not null and rule_id is null means it is an Action
        // check the list of event and reference actions on the link below
        // https://developer.shopware.com/docs/resources/references/core-reference/flow-reference
        'action_name' => 'action.generate.document',
        'config' => \json_encode([
            'documentTypes' => [
                [
                    'documentType' => 'delivery_note',
                    'documentRangerType' => 'document_delivery_note',
                ],
            ],
        ]),
        'position' => 2,
        'display_group' => 1,
        'true_case' => 1, // true
        'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
    ]
);

// the condition's false case sequences
$connection->insert(
    FlowSequenceDefinition::ENTITY_NAME,
    [
        'id' => Uuid::randomBytes(),
        'flow_id' => $flowId,
        'parent_id' => $sequenceConditionId,
        // the sequence has action_name is not null and rule_id is null means it is an Action
        // check the list of event and reference actions on the link below
        // https://developer.shopware.com/docs/resources/references/core-reference/flow-reference
        'action_name' => 'action.add.order.tag',
        'config' => \json_encode([
            'entity' => 'order',
            'tagIds' => ['084dacaa7f3347e9aa4722e9fd5ee6d5' => 'Still love it'],
        ]),
        'position' => 1,
        'display_group' => 1,
        'true_case' => 0, // false
        'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
    ]
);
/* ----- End insert sequences have display_group = 1 -------*/

/* ----- Start insert sequences have display_group = 2 -------*/
$connection->insert(
    FlowSequenceDefinition::ENTITY_NAME,
    [
        'id' => Uuid::randomBytes(),
        'flow_id' => $flowId,
        'parent_id' => null,
        // the sequence has action_name is not null and rule_id is null means it is an Action
        // check the list of event and reference actions on the link below
        // https://developer.shopware.com/docs/resources/references/core-reference/flow-reference
        'action_name' => 'action.generate.document',
        'config' => \json_encode([
            'documentTypes' => [
                [
                    'documentType' => 'invoice',
                    'documentRangerType' => 'document_invoice',
                ],
            ],
        ]),
        'position' => 1,
        'display_group' => 2,
        'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
    ]
);
/* ----- End insert sequences have display_group = 2 -------*/

Last but not least, we need to register the index for flow.indexer or run bin/console dal:refresh:index directly after migrating new migrations to re-index all flows, this will help to build flow.payload (flows only works correctly when its payload is built )

public function update(Connection $connection): void
{
  ...

  $this->registerIndexer($connection, 'flow.indexer');
}

Your implementation is done, migrate your migration and check the result in Administration -> Settings -> Flow Builder

bin/console database:migrate YourPluginName --all

Small tip

You can create your expected flow manually and then create your migration according to its API payload :D image.png