mrk-te/ibexa-migration-bundle2
A migration bundle for Ibexa CMS that brings Laravel-style migration workflows to your project, letting you define, version, and run database and content migrations with simple, repeatable commands. Handy for teams managing evolving Ibexa installations.
This bundle is the maintained-ish port of the abandoned Kaliop eZ-Migration Bundle to Ibexa 5. If you already have Kaliop YAML migrations, this is the shortest path to keeping them; if you are starting fresh, treat it as a content-seeding/deployment tool rather than a general-purpose ORM migration system.
Minimal setup
Require it and check the constraint against your Ibexa 5 / Symfony / PHP versions. Pin at least 3.2.1 — that release fixes the kaliop:migration:generate command, which was broken before it:
composer require mrk-te/ibexa-migration-bundle2:^3.2.1
Pin the exact version in composer.json. With one star and zero dependents you do not want a floating constraint silently changing migration semantics.
Register the bundle in config/bundles.php if Flex did not do it:
MrkTe\IbexaMigrationBundle2\MrkTeIbexaMigrationBundle2::class => ['all' => true],
The exact class name is worth verifying against vendor/mrk-te/ibexa-migration-bundle2/src/ because the fork renamed namespaces from the Kaliop original.
Add the bundle config block. The shape follows Kaliop: one or more migration directories, optionally migration bundles. Confirm the exact config key from the package README or by running bin/console config:dump-reference and grepping for "migration".
# config/packages/mrk_te_ibexa_migration.yaml
mrk_te_ibexa_migration:
migrations_paths:
- '%kernel.project_dir%/migrations'
Create the migration tracking table. The bundle records executed migrations in the database (as Kaliop did). Run the Doctrine schema update in dev, then generate a Doctrine migration for staging/prod:
php bin/console doctrine:schema:update --dump-sql
php bin/console doctrine:schema:update --force
Discover the actual command prefix. As of 3.2.1 the commands are exposed under the Kaliop namespace (the release fixed kaliop:migration:generate), but confirm on your installed version:
php bin/console list | grep -i migration
You should see kaliop:migration:migrate, kaliop:migration:status, kaliop:migration:generate, and kaliop:migration:create.
Where to look first
vendor/mrk-te/ibexa-migration-bundle2/README.md and any docs/ directory — the fork's deltas from Kaliop matter more than the Kaliop docs.src/Migration/Step/ (or equivalent) in the vendor directory. This is the authoritative list of step types that were actually ported. Do not assume every Kaliop step exists.First use case
Seed a content structure on a fresh Ibexa install: one folder, one article, linked by a reference.
# migrations/2026_09_18_000001_seed_homepage.yml
- type: content
mode: create
content_type: folder
parent: "$root_location"
lang: eng-GB
attributes:
name: "Editorial"
reference: editorial_folder
- type: content
mode: create
content_type: article
parent: "$editorial_folder"
lang: eng-GB
attributes:
title: "Welcome"
body: "<p>First seeded article.</p>"
reference: welcome_article
Run status first, then migrate with verbosity:
php bin/console kaliop:migration:status
php bin/console kaliop:migration:migrate -vvv
Verify in the Ibexa back office, then commit the YAML. That is the whole day-to-day loop.
Treat migrations as versioned, append-only files
Use timestamped filenames so execution order is deterministic: 2026_09_18_120000_add_press_release_type.yml. Never edit a migration that has already run in a shared environment; add a new compensating migration instead. The tracking table is the source of truth, not the file contents.
Separate schema/config migrations from content migrations
Content type and language changes are infrastructure; content seeds are environment-specific. Keep them in different directories and run them at different pipeline stages:
migrations_paths:
- '%kernel.project_dir%/migrations/content_types'
- '%kernel.project_dir%/migrations/content'
Run content_types in every environment, and run content only in dev/staging or as an explicit first-deploy seed. This single split prevents most "why did production get demo articles" incidents.
Use references for intra-file dependencies
References (reference: some_name) let later steps in the same migration resolve IDs without hardcoding them. Use them for parent locations, relations, and object attributes:
- type: content
mode: create
content_type: folder
parent: "$root_location"
attributes:
name: "Products"
reference: products_folder
- type: content
mode: create
content_type: product
parent: "$products_folder"
attributes:
name: "Widget"
reference: widget
References are run-scoped. Do not expect $widget to exist in a later migration file or a later migrate invocation. For cross-migration identity, use stable remote IDs that you control.
Prefer idempotent update mode keyed on remote ID
For anything that must converge across environments, create with an explicit remote ID and update by matching it:
- type: content
mode: update
match:
remote_id: "a1b2c3d4e5f6..."
attributes:
title: "Updated title"
This turns a migration into a reconciliation step rather than a one-shot insert. It is the only reliable way to handle content that may already exist from an earlier failed run.
Use kaliop:migration:generate to bootstrap from an existing database
The generate command inspects existing content and emits YAML. It is working again as of 3.2.1 (issue #14), so on 3.2.1+ you can rely on it when porting a legacy site or onboarding onto a database that already has content. Generated migrations are verbose and contain environment-specific IDs; edit them heavily before committing. Treat the output as a starting point, not a deliverable. On versions below 3.2.1, expect this command to fail and plan to hand-write the equivalent migration instead.
Wrap migrations in deploy scripts
Migrations should run non-interactively in CI/CD:
php bin/console kaliop:migration:migrate --no-interaction -vvv
Run status before migrate and fail the deploy if the status output contains pending migrations unexpectedly. Do not run migrations during cache:clear or container warmup; keep them as a discrete deploy step after schema updates and before traffic switchover.
Custom step types are the extension point
When the built-in DSL cannot express something (custom field transformation, external API call, complex relation wiring), implement a step class and register it with the bundle's DI tag. The tag name is the fork's equivalent of Kaliop's kaliop.migration.step; grep the bundle's service definitions for migration.step to find the exact string. Keep custom steps small and idempotent, and test them through a real migration rather than unit tests alone, because the step receives the migration context and reference stack.
Use migration bundles for reusable fixtures
If several projects share a content model, package those migrations in a small bundle and register it in the migration bundle config. This mirrors Kaliop's migration-bundle pattern and keeps the application's migrations/ directory focused on project-specific deltas.
Handle multilingual content explicitly
Set lang per step and add translation steps rather than assuming a default language. A migration that creates content in eng-GB and then updates it in ger-DE should be two clearly ordered steps; mixing languages in one step is a common source of "attribute silently not saved" confusion.
No automatic rollback
Kaliop-style migrations are forward-only. A failure halfway through a file leaves partial content, and the tracking table may or may not mark the migration as run depending on where it failed. Design every migration so that re-running it is safe: use update with match, check-then-create logic, or make the migration small enough to clean up manually. Test in a disposable database before production.
Editing an executed migration does nothing
If a migration is recorded as executed, changing the YAML will not re-run it. You must either add a new migration or manually delete the tracking row (only in dev). This is the most common "my change isn't applying" issue.
References do not survive across runs or files
$some_reference is resolved within the current migration execution. If a later migration needs the same object, resolve it by remote ID or by a repository query, not by reference name. Building a chain of migrations that assume references leak forward will work once and fail forever after.
YAML type coercion will bite you
Unquoted yes, no, on, off, dates, and numeric-looking strings get coerced by the YAML parser before the migration sees them. Quote all attribute values that are semantically strings:
attributes:
title: "Yes"
publish_date: "2026-09-18"
external_code: "00123"
This is especially painful for content type identifiers and remote IDs.
Use identifiers and remote IDs, never numeric IDs
Numeric content/location IDs differ between environments. Use content type identifiers, remote_id, and location remote IDs. If you must use numeric IDs, isolate them in environment-specific migration sets.
Content type changes require cache clearing
After a migration creates or modifies a content type, Ibexa's content type cache may be stale for subsequent steps or for the back office. Run php bin/console cache:clear (or the Ibexa-specific cache command) after type migrations and before content migrations in the same deploy.
Ordering is lexical, not dependency-aware
Files run in filename order within each configured path. There is no dependency graph. If migration B creates content of a type created by migration A, the filename ordering must guarantee A runs first, and the tracking table must not already contain B from a previous partial deploy.
Permissions and user context
Migrations execute through the Ibexa repository API and are subject to the configured user's permissions. A migration that works as admin may fail in a restricted environment. Check whether the command supports a user option in this fork (--user in Kaliop-era commands); if not, ensure the CLI runs with sufficient permissions and document it.
Large datasets are slow
Every content operation goes through the public API, which means events, indexing, and cache invalidation. Bulk-updating tens of thousands of objects via YAML is possible but slow and memory-hungry. For true bulk data backfills, consider a dedicated console command using the repository API with batched flush(), or native SQL when you can safely bypass the API. Reserve migrations for structure and modest content sets.
The fork is a single-maintainer port
With one star, zero dependents, and GPL-2.0 licensing, this is infrastructure you are effectively co-maintaining. Pin a version, read the source when behavior differs from Kaliop docs, and be prepared to vendor-patch or fork. The 3.2.1 fix is a good sign — a broken first-party command was picked up and shipped — but the release surface is still small, so check the issue tracker and recent commits before adopting it for a production deployment.
Debugging checklist
When a migration fails or silently skips:
kaliop:migration:status to confirm whether it is marked executed.-vvv and check var/log/dev.log for the underlying repository exception.type: may be ignored or throw a confusing error depending on the fork's validation.generate misbehaves, confirm your installed version is 3.2.1 or newer (pre-3.2.1 had a broken kaliop:migration:generate).When not to use it
If your need is only to load a handful of fixtures in tests, Ibexa's native fixtures or Doctrine fixtures plus a small custom command may be simpler and better supported. This bundle earns its keep when you have many ordered, reviewable content changes across environments or an existing Kaliop migration suite you want to preserve.
How can I help you explore Laravel packages today?