Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Give me Clean URL Strings (slugs!)

Video not working?



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.


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:

... 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:

... 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:

... lines 1 - 10
class Version20160921253370 extends AbstractMigration
... lines 13 - 15
public function up(Schema $schema)
$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->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!

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1