Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Give me Clean URL Strings (slugs!)

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.

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:

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

18
Login or Register to join the conversation
Mehul-J Avatar
Mehul-J Avatar Mehul-J | posted 1 year ago

Hi, can we run this project with Symfony 5

Reply

Hey Mehul-J!

The concepts in this tutorial should still work just fine on Symfony 5. But if you download the course code, it uses Symfony 3. Many of the core concepts about ManyToMany relationships are more recently covered in https://symfonycasts.com/sc... (which at this moment, is still being released - https://symfonycasts.com/sc... is an almost-identical version for Symfony 4). The unique stuff in this tutorial is really the form collection stuff, which hasn't changed much since Symfony 3.

If you have any questions along the way, you can definitely ask them :).

Cheers!

Reply
msniezko Avatar
msniezko Avatar msniezko | posted 3 years ago

Hi, how to get this running today? I managed to get this working using php 7.2 but my page looks awful, no styling whatsoever. Edit: Managed to get this working by renaming vendor/bootstrap/css/bootstrap.min.css to vendor/bootstrap/css/bootstrap.css in asset() method in twig and refreshing page with ctrl+f5

Reply

Hey Michał,

I'm happy you were able to get it working yourself! And thank you for sharing your solution with others :)

Cheers!

Reply
Default user avatar

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?

Reply

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!

Reply
Default user avatar

That makes sense, thanks

Reply

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.

Reply
Richard Avatar
Richard Avatar Richard | posted 5 years ago

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?

Reply

Hey Richard ,

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!

Reply
Richard Avatar

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.

Reply

Hey Richard ,

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!

Reply
Default user avatar
Default user avatar Nikolay | posted 5 years ago

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?

Reply

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!

Reply
Default user avatar

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

Symfony - the best framework!

Reply

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

Cheers!

Reply
Default user avatar

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?

Reply

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!

Reply
Cat in space

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

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
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "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
    }
}
userVoice