Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

magmatic's avatar

How to write a Laravel migration that depends on existing data?

We are restructuring our database slightly, and all records beginning with a certain word are going to their own table. This is no problem. I'll just write a migration for this. But in developing the new migration, I need sample data to work on.

The problem is that if you run "artisan migrate:fresh --seed", all the migrations will run first, and the seeder will only run afterwards, so my migration won't have any data to work on.

Is there a recommended way to do this? I want to run the seeder before the migration. Or, I want to run the migration after seeding.

Thanks.

0 likes
19 replies
jlrdw's avatar

A side note definitely backup your data first before messing with migrations.

1 like
Snapey's avatar

you could run a seeder within the migration, before changing the tables?

2 likes
Tray2's avatar

I would not use migrations to restructure the database, I would create a SQL script for each step, since it will most likely require several temporary tables to keep the integrity of the data intact, and as @jlrdw says don't forget to backup you data for easy restore.

2 likes
martinbean's avatar

We are restructuring our database slightly, and all records beginning with a certain word are going to their own table. This is no problem. I'll just write a migration for this. But in developing the new migration, I need sample data to work on.

The problem is that if you run "artisan migrate:fresh --seed", all the migrations will run first, and the seeder will only run afterwards, so my migration won't have any data to work on.

Is there a recommended way to do this? I want to run the seeder before the migration. Or, I want to run the migration after seeding.

In your migration, I’d call some sort of class that performs the logic you want:

public function up(): void
{
    app(MoveRecordsBeginningWithCertainWord::class)->execute();
}

You can then test this class separately:

public function testItMovesRecordsBeginningWithCertainWord(): void
{
    // Set up database with some test records

    $this->app->make(MoveRecordsBeginningWithCertainWord::class)->execute();

    // Assert expected records were moved
}

Now that you have encapsulated the logic and it is tested, you can confidently use it in your migration and know data will be moved as you expect.

magmatic's avatar

Thanks for the ideas, everyone! I appreciate it.

I think it would also be handy to be able to skip a migration. I would like to run a fresh database install and seed while skipping my migration. Then, when it's done, I could run a simple "artisan migrate" (without skipping it) to test my migration on some seeded data. This sounds simple and easy.

Laravel doesn't support skipping migrations, does it? Because that would be cool!

martinbean's avatar

Laravel doesn't support skipping migrations, does it? Because that would be cool!

@magmatic It wouldn’t.

Migrations are version control for your database schema. You don‘t skip migrations the same way you don’t “skip” commits when checking out a Git repository.

If a migration relies on records, then you need to insert those records in a previous migration.

jlrdw's avatar

@magmatic

Laravel doesn't support skipping migrations, does it? Because that would be cool!

You don't have to use migrations, you can connect to an existing database.

magmatic's avatar

OK, OK, I hear you. I understand, I think. I'm trying to understand. But what about this situation?

Your website has a number of articles. In the articles table, there is a column called "private" and a column called "published". At some point you decide that instead of a number of flag columns, you need one column called "state", which could be things like "draft", "hidden", "private", "public". (The details are unimportant.)

Wouldn't it make sense to write a migration that adds this "state" column, then loops through all articles to calculate what it should be, then sets it accordingly, then drops the "private" and "published" columns? I would write a migration to do this. In my mind, a migration is the perfect solution.

My original question was, "How would you test this migration if there are no articles in the development database, because it hasn't been seeded yet?"

But now the discussion seems to be that I'm using migrations wrong. If so, can anyone think of a better, cleaner, simpler solution in this case than to use one migration that will automatically get run on deploy?

Thanks for you feedback.

martinbean's avatar

OK, OK, I hear you. I understand, I think. I'm trying to understand. But what about this situation?

Your website has a number of articles. In the articles table, there is a column called "private" and a column called "published". At some point you decide that instead of a number of flag columns, you need one column called "state", which could be things like "draft", "hidden", "private", "public". (The details are unimportant.)

Wouldn't it make sense to write a migration that adds this "state" column, then loops through all articles to calculate what it should be, then sets it accordingly, then drops the "private" and "published" columns? I would write a migration to do this. In my mind, a migration is the perfect solution.

My original question was, "How would you test this migration if there are no articles in the development database, because it hasn't been seeded yet?"

But now the discussion seems to be that I'm using migrations wrong. If so, can anyone think of a better, cleaner, simpler solution in this case than to use one migration that will automatically get run on deploy?

@magmatic I think your problem is, you’re thinking of how to do all of those steps in one, single migration. Break it down. You’d need to do it in multiple steps, just like you described.

You’d first create a migration that adds the state column with a sensible default value, and then a query after that sets the state value for any existing rows:

// 2024_08_23_000000_add_state_column_to_articles_table.php
Schema::table('articles', function (Blueprint $table) {
    $table->string('state')->default('draft');
});

Article::query()->eachById(function (Article $article) {
    $article->update([
        'state' => match (true) {
            // Logic to convert private and published columns to single state
        },
    ]);
});

With the state column added and its value set for each row, you can then drop the old columns in a second migration:

// 2024_08_23_000001_drop_private_and_published_columns_from_articles_table.php
Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('private');
    $table->dropColumn('published');
});

You can now run your migrations in one go, and data will be migrated as well as your tables’ schemas.

automica's avatar

@magmatic if you are developing a feature and running tests to build / confirm your feature works, then you should be running a _testing database that you run your tests against.

providing you have an env.testing you can test your migration works by running the following flag artisan migrate:fresh --seed --env=testing

As others have said, if this is a one time migration, write what you want to do in a command, and then run that directly on your live environment.

To test that works, use factories to seed the test data, call your command, and then run assertions against you database to confirm the data has been transformed correctly.

if you dont want to run the command directly on your live environment - eg if you don't have terminal access, call the command in a migration as that will run once.

magmatic's avatar

@automica You said: "providing you have an env.testing you can test your migration works by running the following flag artisan migrate:fresh --seed --env=testing"

I don't think this is true, because there still won't be any data in the testing database to test on, since it's fresh, and the seed won't happen until after the migration.

magmatic's avatar

@automica You said, "As others have said, if this is a one time migration, write what you want to do in a command, and then run that directly on your live environment."

If I change the database structure in a command instead of a migration, then running the migration files will no longer produce the correct database structure. I need a migration so that the database structure will remain correct.

magmatic's avatar

I propose this:

Instead of skipping arbitrary migrations, we have an option to not run all the way to the end of the list of migrations. This would be similar to running all migrations then doing a rollback, but with the benefit that you don't need to write a "down" function. What do y'all think of that?

martinbean's avatar

@magmatic …or you could just migrate data in your migrations like I’ve mentioned multiple times in this thread now? 🤷‍♂️ Two of those times included my writing out two comprehensive code examples illustrating how to solve the “problem” presented by you.

If migrations were meant to be “paused” or stopped at arbitrary points, then they would support that.

magmatic's avatar

@martinbean Of course I would migrate data in the migration. I assumed I would be doing that from the beginning.

The question was how to get data in there before the migration, in development, since seeding happens after migrating.

martinbean's avatar

@magmatic Data doesn’t appear from thin air. I’ve shown you two different approaches now on how to deal with migrating existing data.

magmatic's avatar

@martinbean I've gone back and re-read all your posts. I think I understand now your proposed solution to this problem.

However, after thinking about it, I think it's simpler to just rename the migration file to ".php.hidden", run a fresh migration, then rename it to ".php", then run migrate to really run it.

And since I'm all about removing complexity and embracing simplicity, this is how I'm going to handle this situation, since it's much simpler. It's a bit of a hack, but it's the best solution I've found so far.

But thanks for your suggestions.

Please or to participate in this conversation.