Data migration
title: Data migration (Migrate API) description: The core architecture for moving data from another system into CPalius: source, destination, transformation, idempotency, rollback and the command line.
Moving content into CPalius from another CMS or from CSV is done with the core's Migrate API. The core carries only the engine and the destinations; code specific to sources such as WordPress, XenForo, MyBB and Joomla lives in the Importer module (covered separately in the Module Documentation). This page explains how the engine works; if you will write your own migration job, start here.
Four concepts
| Concept | Interface | Job |
|---|---|---|
| Migration | MigrationInterface |
A migration job: its id, label, dependencies, source, destination and transformation |
| Source | MigrationSourceInterface |
Gives rows in order with rows(), tells the total with count() |
| Destination | MigrationDestinationInterface |
Writes a row into CPalius with write(), rolls back with delete() |
| Row | MigrationRow |
A non-empty sourceId and a data array |
Ready-made destinations in the core: NodeDestination (content), TermDestination (vocabulary term), UserDestination (user), AssetDestination (media). Ready-made sources: CsvSource, DatabaseSource (from another database, read-only).
A class implementing MigrationInterface gets the cpalius.migration tag automatically.
Rules that make migration safe
- The default is a dry run. The
runcommand writes nothing unless--applyis given; it reports what it would do. - It is idempotent. The
sourceIdof each row is written to a mapping table (migration_id+source_idunique). Running the same migration again does not produce copies; it updates only changed rows (each row'schecksum()is compared). - Rows are isolated. An error in one row does not stop the migration; it is written to the report and the next row is processed.
- Dependencies are ordered. "First users, then posts" is defined with
dependsOn(); the engine runs in topological order. - It streams. Sources read row by row (XMLReader, keyset pagination) without filling memory.
- It is reversible. The mapping is saved after a successful write to the destination;
rollbackdeletes what was written through the mapping.
Command line
php cp-core/bin/console cp:migrate list # defined migrations
php cp-core/bin/console cp:migrate status wordpress.posts # status and the options it accepts
php cp-core/bin/console cp:migrate run wordpress.posts -o file=/path/export.xml # DRY RUN
php cp-core/bin/console cp:migrate run wordpress.posts -o file=/path/export.xml --apply # actually write
php cp-core/bin/console cp:migrate run --apply # all of them, in dependency order
php cp-core/bin/console cp:migrate rollback wordpress.posts --apply
| Option | Meaning |
|---|---|
--apply |
Turns writing on; without it run and rollback only report |
--limit=N |
Stops after the first N source rows (for trying) |
-o name=value |
Passes an option to the migration; can be written more than once |
--json |
Machine-readable output (for CI) |
Do a dry run with --limit=20, read the report, then write the same 20 rows with --apply and see the result in the panel. If you like it, run without a limit; thanks to idempotency the first 20 are not copied.
Writing your own migration
final class OldSiteNews implements MigrationInterface
{
public function id(): string { return 'oldsite.news'; }
public function label(): string { return 'Old site news'; }
public function dependsOn(): array { return ['oldsite.users']; }
public function source(): MigrationSourceInterface { /* DatabaseSource or CsvSource */ }
public function destination(): MigrationDestinationInterface { /* NodeDestination */ }
public function transform(MigrationRow $row): ?MigrationRow
{
// returning null skips the row
return $row->withData([...$row->data, 'type' => 'post', 'status' => 'published']);
}
}
transform() is plain PHP; null skips the row, a MigrationRow goes to the destination. A migration that wants to accept options implements ConfigurableMigrationInterface.
An honest warning
The ready-made drivers (WordPress, XenForo, MyBB, Joomla) have been tested against test data sets, not real installations. When migrating a real site, always start with a dry run, a small limit and a backup; the drivers' quirks (MyBB's linkto and closed fields, Joomla's random table prefix and nested-set root, split article bodies) show up in real data.