Skip to content

Create data migration step

Besides the built-in migrations steps, you can also create custom ones.

To create a custom migration step, you need:

  • A step class, to store any additional data that you might require.
  • A step normalizer, to convert YAML definition into your step class.
  • A step executor, to handle the step.

The following example shows how to create a step that replaces all ezstring fields that have an old company name with "New Company Name".

Create step class

First, create a step class, in src/Migrations/Step/ReplaceNameStep.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

declare(strict_types=1);

namespace App\Migrations\Step;

use Ibexa\Migration\ValueObject\Step\StepInterface;

final class ReplaceNameStep implements StepInterface
{
    private string $replacement;

    public function __construct(?string $replacement = null)
    {
        $this->replacement = $replacement ?? 'New Company Name';
    }

    public function getReplacement(): string
    {
        return $this->replacement;
    }
}

Create normalizer

Then you need a normalizer to convert data that comes from YAML into a step object, in src/Migrations/Step/ReplaceNameStepNormalizer.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

declare(strict_types=1);

namespace App\Migrations\Step;

use Ibexa\Contracts\Migration\Serializer\AbstractStepNormalizer;
use Ibexa\Migration\ValueObject\Step\StepInterface;

/**
 * @extends \Ibexa\Contracts\Migration\Serializer\AbstractStepNormalizer<\App\Migrations\Step\ReplaceNameStep>
 */
final class ReplaceNameStepNormalizer extends AbstractStepNormalizer
{
    protected function normalizeStep(
        StepInterface $object,
        string $format = null,
        array $context = []
    ): array {
        assert($object instanceof ReplaceNameStep);

        return [
            'replacement' => $object->getReplacement(),
        ];
    }

    protected function denormalizeStep(
        $data,
        string $type,
        string $format,
        array $context = []
    ): ReplaceNameStep {
        return new ReplaceNameStep($data['replacement'] ?? null);
    }

    public function getHandledClassType(): string
    {
        return ReplaceNameStep::class;
    }

    public function getType(): string
    {
        return 'company_name';
    }

    public function getMode(): string
    {
        return 'replace';
    }
}

Then, tag the step normalizer, so it's recognized by the serializer used for migrations.

1
2
3
4
    App\Migrations\Step\ReplaceNameStepNormalizer:
        tags:
            - 'ibexa.migrations.serializer.step_normalizer'
            - 'ibexa.migrations.serializer.normalizer'

Create executor

And finally, create an executor to perform the step, in src/Migrations/Step/ReplaceNameExecutor.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php

declare(strict_types=1);

namespace App\Migrations\Step;

use Ibexa\Contracts\Core\Repository\ContentService;
use Ibexa\Contracts\Core\Repository\Values\Filter\Filter;
use Ibexa\Contracts\Migration\StepExecutor\AbstractStepExecutor;
use Ibexa\Core\FieldType\TextLine\Value;
use Ibexa\Migration\ValueObject\Step\StepInterface;

final class ReplaceNameStepExecutor extends AbstractStepExecutor
{
    private ContentService $contentService;

    public function __construct(
        ContentService $contentService
    ) {
        $this->contentService = $contentService;
    }

    protected function doHandle(StepInterface $step)
    {
        assert($step instanceof ReplaceNameStep);

        $contentItems = $this->contentService->find(new Filter());

        foreach ($contentItems as $contentItem) {
            $struct = $this->contentService->newContentUpdateStruct();

            foreach ($contentItem->getFields() as $field) {
                if ($field->fieldTypeIdentifier !== 'ezstring') {
                    continue;
                }

                if ($field->fieldDefIdentifier === 'identifier') {
                    continue;
                }

                if (str_contains($field->value, 'Company Name')) {
                    $newValue = str_replace('Company Name', $step->getReplacement(), $field->value);
                    $struct->setField($field->fieldDefIdentifier, new Value($newValue));
                }
            }

            try {
                $content = $this->contentService->createContentDraft($contentItem->contentInfo);
                $content = $this->contentService->updateContent($content->getVersionInfo(), $struct);
                $this->contentService->publishVersion($content->getVersionInfo());
            } catch (\Throwable $e) {
                // Ignore
            }
        }

        return null;
    }

    public function canHandle(StepInterface $step): bool
    {
        return $step instanceof ReplaceNameStep;
    }
}

Tag the executor with ibexa.migrations.step_executor tag.

1
2
3
    App\Migrations\Step\ReplaceNameStepExecutor:
        tags:
            - 'ibexa.migrations.step_executor'

Then you can create a migration file that represents this step in your application:

1
2
3
-   type: company_name
    mode: replace
    replacement: 'New Company Name' # as declared in normalizer, this is optional