fndmiranda/data-migration
A Laravel package for managing data migrations separately from schema migrations. It provides versioned data migration classes and Artisan commands to transform, move, backfill, and roll back application data safely during deployments and refactors.
Install the package and publish its assets:
composer require fndmiranda/data-migration
php artisan vendor:publish --provider="Fndmiranda\DataMigration\DataMigrationServiceProvider"
Then run the package's own schema migration so it can track which data migrations have executed (it uses its own history table, separate from Laravel's migrations table).
Confirm the command surface, since names have shifted across versions:
php artisan list | grep data
You'll typically see something like data-migration:make, data-migration:run, data-migration:rollback, data-migration:status, and possibly data-migration:refresh.
First use case: reference / lookup data that must exist in every environment and evolve over time — roles, permissions, countries, currencies, default feature flags. These are the classic case where seeders don't fit (they're not tracked and get re-run) and schema migrations don't fit (you don't want data tied to Schema:: calls).
Generate your first one:
php artisan data-migration:make CreateDefaultRoles
That drops a file in the configured directory (commonly database/data-migrations/). Fill in up() to insert/update rows and down() to reverse them.
Where to look first:
config/data-migration.php — path, tracking table name, connection.make — the base class and available helpers.Reference-data sync with upserts. Write migrations as idempotent upserts keyed by a natural key (slug, code), not by auto-increment ID:
public function up()
{
$this->updateOrCreate('roles', ['slug' => 'admin'], ['name' => 'Administrator']);
$this->updateOrCreate('roles', ['slug' => 'editor'], ['name' => 'Editor']);
}
public function down()
{
$this->deleteWhere('roles', ['slug' => ['admin', 'editor']]);
}
Treat data migrations like schema migrations, not seeders. Append-only. Once a file has run in an environment, do not edit it — add a new migration. The tracking table records "this ran once," not "this data is still correct," so editing an executed file changes nothing in existing environments.
Environment-targeted data. Guard demo/dev rows so they never land in production:
if (app()->environment('local', 'staging')) {
// sample customers, test products
}
For larger divergence, use per-environment subdirectories if the package supports path config; otherwise a single guard at the top of up().
Chunking large datasets. Wrap writes in DB::transaction() and chunk reads/writes so a crash doesn't leave partial state with no tracking record. For raw table syncs:
DB::transaction(function () {
foreach (array_chunk($rows, 500) as $batch) {
$this->insert('countries', $batch);
}
});
Deployment pipeline. Order matters — schema first, data second:
php artisan migrate --force
php artisan data-migration:run --force
The --force flag is required in production because the command confirms interactively.
Rollback strategy. Implement down() with a deleteWhere keyed by the same identifiers used in up(). Prefer soft semantics (set a status column) over hard deletes for anything referenced by user data. Confirm whether data-migration:rollback rolls back one step or the last batch before relying on it.
Shared helpers via a base class. Extend the package's DataMigration base and add domain helpers (syncPermissions(), updateOrCreateByCode()), then have generated migrations extend yours. Keep those helpers idempotent.
Testing. In test setup, run the data migrations against the test DB and assert counts/values. If the tracking table pollutes tests, point the config's tracking table at an in-memory or refreshed table per test suite.
migrations. Dropping it re-runs every data migration; back up or never truncate it outside local.deleteWhere won't trigger deleted_at unless you implement it. JSON/array columns need explicit json_encode or the package's cast handling — verify.--force in production. Missing it in CI silently fails the step if output isn't checked; also watch for prompts hanging non-interactive jobs.data-migration:status appear empty. Keep the config stable across environments.--pretend/dry-run if available; otherwise run against a clone. data-migration:status should be your first check — it tells you what the tracker thinks has run versus what's on disk.data-migration:sync that re-applies upserts without touching history.composer.json, read composer.json for the supported Laravel range before upgrading, and be ready to read the source for exact helper signatures (updateOrCreate, deleteWhere, etc.) — they're the fastest way to resolve "method not found" surprises.How can I help you explore Laravel packages today?