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-09-27 Victor Bocharsky

    Hey maxii123 ,

    Unfortunately, we can't cover all the cases, because it'll be to wide for this tutorial. Basically, we show in our tutorials new projects. It's up to you to follow tutorials on your existent project, and that's great for learning, but you need to be ready for extra work.

    It'd be a good idea to add a tip about generating slugs with SQL if it would be a good practice here, but it's not, especially if we're talking about legacy projects which have big data. First of all, you need to use a library to generate URI-compatible slugs, and then you need a custom logic written in PHP to make those slugs unique. So doing all of this in Doctrine migration is not a good idea - better use custom migration command. And probably you won't have a robust solution with one simple SQL query. But more complex example is out of the range of this course.

    Cheers!

  • 2017-09-27 maxii123

    Thanks for the reply. I get what you're saying but I meant to suggest (not very well!) that the course notes might include the one operation SQL to generate the slugs since many of us are learning here to migrate existing systems.

  • 2017-09-05 Victor Bocharsky

    Hey Nina,

    Unique fields could be blank, i.e. could be null if you allow it. But unique fields cannot be the same, I mean, cannot be equal to the same string whether it's an empty string, etc, because an empty string is not equal to NULL - those are different things.

    Yes, slug is a text field which means to be a valid to be used in URLs. And slugs are more readable than IDs, so it's more popular for SEO. For example, "/page/about" is more readable than "/page/28", where "about" is a unique slug of the article and 28 is its ID. So, you can use IDs for that, but slugs are more "hipster" way now :)

    Cheers!

  • 2017-09-05 Nina

    Hello, I don't really understand why we couldn't to add field 'slug' without
    php bin/console doctrine:database:drop --force
    because a unique field couldn't be blank?

    and what does 'slug' mean
    field for creating a unique URL?
    why we can not to use 'id' for this?

  • 2017-09-04 Victor Bocharsky

    Да, обычно каждый новый курс содержит какие-то небольшие правки и дополнения для того чтобы подготовить его к новой теме. Поэтому лучше начинать со стартового кода. Если вы работаете с Git - вы можете скачать финишный код предыдущего курса, добавить его в Git (закомитить) и потом перезаписать файлы стартовым кодом из нового курса и команда "git diff" покажет вам разницу между финишным кодом предыдущего курса и стартовым кодом нового. Это в случае если вам интересно что конкретно было изменено. Потом, эти изменения сможете применить конкртено на вашем коде и продолжать новый курс на своем старом проекте.

    Cheers!

  • 2017-09-04 Nikolay

    Глянул, вы с Украины Виктор. Я с английским не очень, поэтому отвечу по русски. Спасибо за ответ - я оплатил месячную подписку и все что нужно (стартовый код) скачал. Код от предыдущего курса не подошел - для данного курса вы, видимо, делали добавления какие то. Спасибо за ваш труд и поддержку новичков!

    Symfony - the best framework!

  • 2017-09-04 Victor Bocharsky

    Hey Nikolay,

    Yes, you can. "Getting Crazy with Form Themes" course was the base for this course. But keep in mind that the finish code from previous course not a start code for the new one because often we do some tweaks before starting a new course. And our GitHub repositories contains only start code. If you want the finish code of the course, you need to download it on any chapter page, but you need to have an active subscription or own a course to do it.

    Cheers!

  • 2017-09-02 Nikolay

    Hi! Can I use source code from previous course, "Getting Crazy with Form Themes" (final version), or for this course, changes were made in the composition fail or the modified code? GitHub has the source code for the start of this course?

  • 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?