Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine


Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

We created an entity class! But... that's it. The corresponding table does not yet exist in our database.

Let's think. In theory, Doctrine knows about our entity, all of its properties and their ORM\Column attributes. So... shouldn't Doctrine be able to make that table for us automatically? Yes! It can.

The make:migration Command

When we installed Doctrine earlier, it came with a migrations library that's amazing. Check it out! Whenever you make a change to your database structure - like adding a new entity class, or even adding a new property to an existing entity, you should spin over to your terminal and run:

symfony console make:migration

In this case, I'm running symfony console because this is going to talk to our database. Run that and... perfect! It created one new file in a migrations/ directory with a timestamp for today's date. Let's go check it out! Find migrations/ and open the new file.

... lines 1 - 12
final class Version20220718170654 extends AbstractMigration
public function getDescription(): string
return '';
public function up(Schema $schema): void
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SEQUENCE vinyl_mix_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE vinyl_mix (id INT NOT NULL, title VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, track_count INT NOT NULL, genre VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('COMMENT ON COLUMN vinyl_mix.created_at IS \'(DC2Type:datetime_immutable)\'');
public function down(Schema $schema): void
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE vinyl_mix_id_seq CASCADE');
$this->addSql('DROP TABLE vinyl_mix');

This holds a class with up() and down() methods... though I never run migrations in the "down" direction, so we'll focus only on up(). And... this is great! The migrations command saw our VinylMix entity, realized that its table was missing in the database, and generated the SQL needed in Postgres to create it, including all of the columns. That was so easy.

Executing the Migration

Ok... so how do we execute this migration? Back at your terminal, run:

symfony console doctrine:migrations:migrate

Say y to confirm and... beautiful! It tells us that it's Migrating up to that specific version. It seems... like that worked! To make sure, you can try another bin/console command: symfony console doctrine:query:sql with SELECT * FROM vinyl_mix.

symfony console doctrine:query:sql 'SELECT * FROM vinyl_mix'

When we try that... whoops! Pardon my typo... nothing to see here. Try that again and... perfect! We didn't get an error! It just says that The query yielded an empty result set. If that table did not exist, like vinyl_foo, Doctrine would have screamed at us.

So, the migration did run!

How Migrations Work

This beautiful system deserves some explanation. Run

symfony console doctrine:migrations:migrate

again. Check it out! It's smart enough to avoid executing that migration a second time! It knows that it already did that. But... how? Try running a different command:

symfony console doctrine:migrations:status

This gives some general info about the migration system. The most important part is in Storage where it says Table Name and doctrine_migration_versions.

Here's the deal: the first time we executed the migration, Doctrine created this special table, which literally stores a list of all of the migration classes that have been executed. Then, each time we run doctrine:migrations:migrate, it looks in our migrations/ directory, finds all the classes, checks the database to see which have not already been executed, and only calls those. Once the new migrations finish, it adds them as rows to the doctrine_migration_versions table.

You can visualize this table by running:

symfony console doctrine:migrations:list

It sees our one migration and knows it already ran it. It even has the date!

This is cool... but let's push it further. Next, let's add a new property to our entity and generate a second migration to add the column.

Leave a comment!

Login or Register to join the conversation
Roy Avatar

If I try symfony console make:migration it gives an error that the metadata storage is not up to date, please run the sync-metadata-storage command to fix this issue.

Running that command does nothing.

I know the database works, because I already have it set up for accepting a login/registration, so I know the database connection URL etc works. that it accepts the user entity and all the fields connected.

The fun thing is, when I make a new database (named mixed_vinyls and change the database URL to reflect this), without any previous entities linked to it, it does work, it does migrate. But that's not particularly useful.

The web, stackoverflow, github etc, seem convinced this is a metadata / .env issue, but that can't be as the database does work.

Roy Avatar
Roy Avatar Roy | Roy | posted 2 months ago | edited

Edit to add,

1) created a new database as mentioned, it does not migrate the VinylMix entity but it does migrate the User entity I have for the login stuff.
2) going back to a prior github version (I push after every video) and using the old (mixed_vinyl) database gives the same error message of metadata storage is not up to date.

Edit again:
It's fixed.

I needed to add mariadb-10.5.2 to my database URL string, which wasn't necesary when I made the first migration with the user entity....

Programming is weird....


Hey Roy!

Programming is weird....

Yup :p.

For reference about this mystery, if you had run bin/console doctrine:migrations:sync-metadata-storage that might have done it (the command incorrectly says to use sync-metadata-storage for confusing reasons that aren't important). I believe this is caused when you get a new version of doctrine migrations and they've changed how the store the migrations in the database. Running this command will "alter" the table to match whatever the new structure is.


Roy Avatar

Learning this for my first internship after my education only teaches minimal and barebones HTML/CSS/PHP etc....

Decided to buy these courses and it's teaching me more already than school did in 3.5 years! Just... I have so many questions that sort of get answered. Thanks for all the replies!


Ah, I'm so thrilled!!! Keep up the good work - happy to answer any questions along the way :)

Roy Avatar

Heh, I wondered why the reply after 4 days...

Things so far work fine. Though I do seem to be missing somethings here and there. Have you, in any episode, handled how we can make the form push to the database? As mentioned before, school done very minimal html / css, never even explained frameworks existed... So after being hyped up that I was ready for an internship I now face the reality that, well, I clearly am not! ha!

Anyways, thanks again for all the support and episodes!


Hey @Roy!

Have you, in any episode, handled how we can make the form push to the database?

Do you mean, submitting a form, taking that data, and finally saving it to the database? Check out out https://symfonycasts.com/screencast/symfony-forms tutorial if you haven't yet. It is actually built on an old version of Symfony (we'll update it next year), but that's because the form system hasn't changed much :).

As mentioned before, school done very minimal html / css, never even explained frameworks existed... So after being hyped up that I was ready for an internship I now face the reality that, well, I clearly am not! ha!

The real world of web dev is complex! But you're doing the right stuff :)

Erik-S Avatar

I want to ask if it's at all possible to work with an existing database, for example xampp db. I have been searching for the answer for a few days and no one seems to know how to do that.


Hey Erik!

Absolutely :). And it's super easy:

1) Don't bother starting Docker - you just don't need this.
2) Look inside .env for the DATABASE_URL environment variable. Copy whatever one (e.g. postgres, mysql) that matches your setup.
3) Create a .env.local file, paste that DATABASE_URL inside, then customize it for your real database name, username, password, etc

That's it! You could also modify .env directly... the downside being that your local machine database credentials would get committed to the repository. But .env.local is ignored from git.

Let me know if that helps!

Benoit-L Avatar
Benoit-L Avatar Benoit-L | posted 1 year ago

Hi there,

This line does not work as such : symfony console doctrine:query:sql 'SELECT * FROM vinyl_mix'

Too many arguments to "doctrine:query:sql" command, expected arguments "sql".



Hey Benoit,

It seems like your terminal does not like single quotes ' try surrounding your SQL code with double quotes "SELECT * FROM vinyl_mix


Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.12", // 2.12.3
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.2", // v6.2.6
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/asset": "6.1.*", // v6.1.0
        "symfony/console": "6.1.*", // v6.1.2
        "symfony/dotenv": "6.1.*", // v6.1.0
        "symfony/flex": "^2", // v2.2.2
        "symfony/framework-bundle": "6.1.*", // v6.1.2
        "symfony/http-client": "6.1.*", // v6.1.2
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.1.*", // v6.1.0
        "symfony/runtime": "6.1.*", // v6.1.1
        "symfony/twig-bundle": "6.1.*", // v6.1.1
        "symfony/ux-turbo": "^2.0", // v2.3.0
        "symfony/webpack-encore-bundle": "^1.13", // v1.15.1
        "symfony/yaml": "6.1.*", // v6.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.1
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.1.*", // v6.1.0
        "symfony/maker-bundle": "^1.41", // v1.44.0
        "symfony/stopwatch": "6.1.*", // v6.1.0
        "symfony/web-profiler-bundle": "6.1.*", // v6.1.2
        "zenstruck/foundry": "^1.21" // v1.21.0