Skip to content

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 run command writes nothing unless --apply is given; it reports what it would do.
  • It is idempotent. The sourceId of each row is written to a mapping table (migration_id + source_id unique). Running the same migration again does not produce copies; it updates only changed rows (each row's checksum() 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; rollback deletes 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)
Try small first

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.

Last updated: 13.10.2026