Buy

Yes! Collections! Ladies and gentleman, this course is going to take us somewhere special: to the center of two topics that each, single-handedly, have the power to make you hate Doctrine and hate Symfony forms. Seriously, Doctrine and the form system are probably the two most powerful things included in the Symfony Framework... and yet... they're also the two parts that drive people insane! How can that be!?

The answer: collections. Like, when you have a database relationship where one Category is related to a collection of Products. And for forms, it's how you build a form where you can edit that category and add, remove or edit the related products all from one screen. If I may, it's a collection of chaos.

But! But, but but! I have good news: if we can understand just a few important concepts, Doctrine collections are going to fall into place beautifully. So let's take this collection of chaos and turn it into a collection of.. um... something awesome... like, a collection of chocolate, or ice cream. Let's do it!

Code and Setup!

You should definitely code along with me by downloading the course code from this page, unzipping it, and then finding the start/ directory. And don't forget to also pour yourself a fresh cup of coffee or tea: you deserve it.

That start/ directory will have the exact code that you see here. Follow the instructions in the README.md file: it will get your project setup.

The last step will be to open a terminal, move into the directory, and start the built-in PHP web server with:

./bin/console server:run

Now, head to your browser and go to http://localhost:8000 to pull up our app: Aquanote! Head to /genus: this lists all of the genuses in the system, which is a type of animal classification.

Tip

The plural form of genus is actually genera. But irregular plural words like this can make your code a bit harder to read, and don't work well with some of the tools we'll be using. Hence, we use the simpler, genuses.

Clean, Unique URLs

Before we dive into collection stuff, I need to show you something else first. Don't worry, it's cool. Click one of the genuses. Now, check out the URL: we're using the name in the URL to identify this genus. But this has two problems. First, well, it's kind of ugly: I don't really like upper case URLs, and if a genus had a space in it, this would look really ugly - nobody likes looking at %20. Second, the name might not be unique! At least while we're developing, we might have two genuses with the same name - like Aurelia. If you click the second one... well, this is actually showing me the first: our query always finds only the first Genus matching this name.

How could I let this happen!? Honestly, it was a shortcut: I wanted to focus on more important things before. But now, it's time to right this wrong.

What we really need is a clean, unique version of the name in the url. This is commonly called a slug. No, no, not the slimy animal - it's just a unique name.

Create the slug Field

How can we create a slug? First, open the Genus entity and add a new property called slug:

165 lines src/AppBundle/Entity/Genus.php
... lines 1 - 12
class Genus
{
... lines 15 - 27
/**
* @ORM\Column(type="string", unique=true)
*/
private $slug;
... lines 32 - 163
}

We will store this in the database like any other field. The only difference is that we'll force it to be unique in the database.

Next, go to the bottom and use the "Code"->"Generate" menu, or Command+N on a Mac, to generate the getter and setter for slug:

165 lines src/AppBundle/Entity/Genus.php
... lines 1 - 12
class Genus
{
... lines 15 - 154
public function getSlug()
{
return $this->slug;
}
public function setSlug($slug)
{
$this->slug = $slug;
}
}

Finally, as always, generate a migration. I'll open a new terminal tab, and run:

./bin/console doctrine:migrations:diff

Open that file to make sure it looks right:

37 lines app/DoctrineMigrations/Version20160921253370.php
... lines 1 - 10
class Version20160921253370 extends AbstractMigration
{
... lines 13 - 15
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE genus ADD slug VARCHAR(255) NOT NULL');
$this->addSql('CREATE UNIQUE INDEX UNIQ_38C5106E989D9B62 ON genus (slug)');
}
... lines 24 - 27
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP INDEX UNIQ_38C5106E989D9B62 ON genus');
$this->addSql('ALTER TABLE genus DROP slug');
}
}

Perfect! It adds a column, and gives it a unique index. Run it:

./bin/console doctrine:migrations:migrate

Ah, Migration Failed!

Oh no! It failed! Why!? Since we already have genuses in the database, when we try to add this new column... which should be unique... every genus is given the same, blank string. If we had already deployed this app to production, we would need to do a bit more work, like make the slug field not unique at first, write a migration to generate all of the slugs, and then make it unique.

Fortunately we haven't deployed this yet, so let's take the easy road. Drop the database:

./bin/console doctrine:database:drop --force

Then recreate it, and run all of the migrations from the beginning:

./bin/console doctrine:database:create
./bin/console doctrine:migrations:migrate

Much better. So.... how do we actually set the slug field for each Genus?

Leave a comment!

  • 2017-07-24 Victor Bocharsky

    Hey maxii123 ,

    Unfortunately, I don't have an example of it, but for such a big and complex tasks we don't use Doctrine migrations. Well, we do use migrations to actually add a column, like slug one, but also we create a one-time migration Symfony command and after Doctrine migrations were executed, run this command to actually populate slugs for all the entities. The problem is that task takes some time, so it's not a good idea to run it in Doctrine migrations because this task could simple failed and your DB will left unsynced. But if your one-time migration command will fail, no problem, just restart it again. Of course, you need to make sure it's safe to run this command more than one time, you just need to skip already handled entities. That's it.

    Cheers!

  • 2017-07-23 maxii123

    Do you have the example code for an already deployed database? Can one just run a single line of code to poulate the slug field?

  • 2016-11-09 Victor Bocharsky

    Btw, I *think* you could manually catch exception with try-catch statement in up() method of migration and do some revert there, but be careful, this way you could missing that something goes wrong and to be honest I have never done it. And probably it's pointless if you have a lot of addSql() calls in migration.

  • 2016-11-09 Johan

    That makes sense, thanks

  • 2016-11-09 Victor Bocharsky

    Hey Johan,

    Actually, everything is much easier, probably for safety. When exception occurs during migration - everything before the failed SQL query is kept, i.e. doesn't reverted. But you can revert some migrations manually using `doctrine:migrations:execute MIGRATION_VERION --down` command. However in most cases it could fail too (especially if exception was thrown in the middle of migration with many separate addSql() calls, but it depends). That's why you probably need to manually execute only *some* queries directly on your SQL server.

    Cheers!

  • 2016-11-08 Johan

    A bit unrelated to this particular tutorial but do you know what happens when an exception occurs during migration? Let's say I have 3 migration files (that are not ran yet) X, Y and Z and an exception occurs while running Z. Does it revert the database to the state it was in *before* running X or does it revert to the state before running Z? Or something else maybe?