Sluggable: Doctrine Extensions

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The whole point of the slug is to be a URL-safe version of the name. And, ideally, this wouldn't be something we need to set manually... or even think about! In a perfect world, we would be able to set the name of a Question, save and something else would automatically calculate a unique slug from the name right before the INSERT query.

We accomplished this in our fixtures, but only there. Let's accomplish this everywhere.

Hello StofDoctrineExtensionsBundle

To do that, we're going to install another bundle. Google for StofDoctrineExtensionsBundle and find its GitHub page. And then click over to its documentation, which lives on Symfony.com. This bundle gives you a bunch of superpowers for entities, including one called Sluggable. And actually, the bundle is just a tiny layer around another library called doctrine extensions.

This is where the majority of the documentation lives. Anyways, let's get the bundle installed. Find your terminal and run:

composer require stof/doctrine-extensions-bundle

You can find this command in the bundle's documentation.

Contrib Recipes

And, oh, interesting! The install stops and says:

The recipe for this package comes from the contrib repository, which is open to community contributions. Review the recipe at this URL. Do you want to execute this recipe?

When you install a package, Symfony Flex looks for a recipe for that package... and recipes can live in one of two different repositories. The first is symfony/recipes, which is the main recipe repository and is closely guarded: it's hard to get recipes accepted here.

The other repository is called symfony/recipes-contrib. This is still guarded for quality... but it's much easier to get recipe accepted here. For that reason, the first time you install a recipe from recipes-contrib, Flex asks you to make sure that you want to do that. So you can say yes or I'm actually going to say P for yes, permanently.

I committed my changes before recording recording, so when this finishes I'll run,

git status

to see what the recipe did! Ok: it enabled the bundle - of course - and it also created a new config file stof_doctrine_extensions.yaml. Let's go check that out: config/packages/stof_doctrine_extensions.yaml.

# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html
# See the official DoctrineExtensions documentation for more details: https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc/
stof_doctrine_extensions:
default_locale: en_US

Ok... nothing too interesting yet.

Activating Sluggable in Config

As we saw, this bundle comes with a bunch of special features for entities. And each time you want to use a feature, you need to enable it in this config file. The first behavior we want is sluggable. To enable it add orm: - because we're using the Doctrine ORM:

... lines 1 - 2
stof_doctrine_extensions:
default_locale: en_US
orm:
... lines 6 - 8

and then default:, because we want to enable this on our default entity manager. That's... really not important except in edge cases where you have multiple database connections.

... lines 1 - 2
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
... lines 7 - 8

Then, sluggable: true.

... lines 1 - 2
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
sluggable: true

That's it! Well... sort of. This won't make any real difference in our app yet. But, internally, the sluggable feature is now active.

Before we start using it, in QuestionFactory, remove the code that sets the slug. I'll delete this logic, but keep an example function for later.

... lines 1 - 20
final class QuestionFactory extends ModelFactory
{
... lines 23 - 40
protected function initialize(): self
{
// see https://github.com/zenstruck/foundry#initialization
return $this
//->afterInstantiate(function(Question $question) { });
;
}
... lines 48 - 52
}

Now, temporarily, if we reload our fixtures with:

symfony console doctrine:fixtures:load

Yep! A huge error because slug is not being set.

The @Gedmo\Slug Annotation

So how do we tell the Doctrine extensions library that we want the slug property to be set automatically? The library works via annotations. In the Question entity, above the slug property, add @Gedmo\Slug() - making sure to autocomplete this so that PhpStorm adds the use statement for this annotation.

The @Gedmo\Slug annotation has one required option called fields={}. Set it to name.

... lines 1 - 6
use Gedmo\Mapping\Annotation as Gedmo;
... lines 8 - 11
class Question
{
... lines 14 - 25
/**
* @ORM\Column(type="string", length=100, unique=true)
* @Gedmo\Slug(fields={"name"})
*/
private $slug;
... lines 31 - 131
}

Done! The slug will now be automatically set right before saving to a URL-safe version of the name property.

Back at the terminal, try the fixtures now:

symfony console doctrine:fixtures:load

No errors! And on the homepage... yes! The slug looks perfect. We now never need to worry about setting the slug manually.

Doctrine's Event System

Internally, this magic works by leveraging Doctrine's event or "hook" system. The event system makes it possible to run custom code at almost any point during the "lifecycle" of an entity. Basically, you can run custom code right before or after an entity is inserted or updated, right after an entity is queried for or other times. You do this by creating an event subscriber or entity listener. We do have an example of an entity listener in our API Platform tutorial if you're interested.

Next, let's add two more handy fields to our entity: createdAt and updatedAt. The trick will be to have something automatically set createdAt when the entity is first inserted and updatedAt whenever it's updated. Thanks to Doctrine extensions, you're going to love how easy this is.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.8", // 1.8.2
        "doctrine/doctrine-bundle": "^2.1", // 2.1.0
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.7", // v2.7.3
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "knplabs/knp-time-bundle": "^1.11", // v1.12.0
        "sensio/framework-extra-bundle": "^5.5", // v5.6.1
        "sentry/sentry-symfony": "^3.4", // 3.5.2
        "stof/doctrine-extensions-bundle": "^1.4", // v1.4.0
        "symfony/asset": "5.1.*", // v5.1.2
        "symfony/console": "5.1.*", // v5.1.2
        "symfony/dotenv": "5.1.*", // v5.1.2
        "symfony/flex": "^1.3.1", // v1.9.0
        "symfony/framework-bundle": "5.1.*", // v5.1.2
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/stopwatch": "5.1.*", // v5.1.2
        "symfony/twig-bundle": "5.1.*", // v5.1.2
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.1.*", // v5.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.0.4
        "twig/twig": "^2.12|^3.0" // v3.0.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.1
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.20.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.1.0
    }
}