Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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:^1.5.0"

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

36
Login or Register to join the conversation
Marcus S. Avatar
Marcus S. Avatar Marcus S. | posted 2 years ago

If you are running into an error with messages like Can only install one of: doctrine/common[v2.9.0, 3.0.2]. while installing the stof-bundle, it is most likely because your doctrine version is too new. At the time of writing, the orm bundle installs doctrine 3.0.2, but the stof bundle requires a 2.x version of doctrine. Thus, the dependency cannot be fulfilled and the installation breaks. To be able to install the bundle nevertheless, you will have to downgrade doctrine. Use the following:
symfony composer require doctrine/common:^2.7 --update-with-dependencies
After that, you should be able to install the stof doctrine extension bundle

1 Reply

Hi!

Just an update - this should not be a problem anymore - we've been waiting for 2 new releases (from doctrine/extensions and stof-doctrine-extensions-bundle) and both have now been released. 🎉

Cheers!

Reply
akincer Avatar

This fails due to data fixtures when I try it.

Reply

Hey Aaron, could you tell me what error you got?

Reply
akincer Avatar

Hey Diego, sure here's the full composer output:

./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Restricting packages listed in "symfony/symfony" to "5.1.*"
Your requirements could not be resolved to an installable set of packages.

Problem 1
- doctrine/data-fixtures 1.4.4 requires doctrine/common ^2.13|^3.0 -> satisfiable by doctrine/common[2.13.0, 2.13.1, 2.13.2, 2.13.3, 2.13.x-dev, 3.0.0, 3.0.1, 3.0.2, 3.0.x-dev, 3.1.x-dev] but these conflict with your requirements or minimum-stability.
- doctrine/data-fixtures 1.4.4 requires doctrine/common ^2.13|^3.0 -> satisfiable by doctrine/common[2.13.0, 2.13.1, 2.13.2, 2.13.3, 2.13.x-dev, 3.0.0, 3.0.1, 3.0.2, 3.0.x-dev, 3.1.x-dev] but these conflict with your requirements or minimum-stability.
- doctrine/data-fixtures 1.4.4 requires doctrine/common ^2.13|^3.0 -> satisfiable by doctrine/common[2.13.0, 2.13.1, 2.13.2, 2.13.3, 2.13.x-dev, 3.0.0, 3.0.1, 3.0.2, 3.0.x-dev, 3.1.x-dev] but these conflict with your requirements or minimum-stability.
- Installation request for doctrine/data-fixtures (locked at 1.4.4) -> satisfiable by doctrine/data-fixtures[1.4.4].


Installation failed, reverting ./composer.json to its original content.
exit status 2

Reply

Oh, I think you only have to update the "doctrine/data-fixtures" library and it should do the trick

Reply
Default user avatar
Default user avatar Aaron Kincer | MolloKhan | posted 2 years ago

I did composer update but data-fixtures didn't have any updates. Trying again resulted in the same error.

Reply

Hmm, interesting... have you tried this composer require doctrine/common --update-with-dependencies

Reply
akincer Avatar

Just got time to test this morning and it looks like the issue with the extensions bundle has been addressed.

1 Reply

those are excellent news! Let's keep rocking

Reply

Hi Marcus S.!

Yes, thanks for sharing! Doctrine recently released a few new major versions (doctrine/common 3 and doctrine/persistence 2) and it's temporarily wreaking a small bit of havoc on things :p.You described it very well and the solution.

Thanks!

Reply
Lechu85 Avatar

Hi there :)
I have a problem. I only needs to generate a Slugg if the private field $slugManual == false

How can i do it?

/**
* @var string
*
* @ORM\Column(name="prod_permalink", type="string", length=255, nullable=false)
* @Gedmo\Slug(fields={"name"}) // Only if slugManual is false.
*/
private $slug;

/**
* @var bool
*
* @ORM\Column(name="prod_permalink_manual", type="boolean", nullable=false)
*/
private $slugManual = false;

Reply

Hey Leszek C.!

Hmm! I'm not sure this is easily possible. There is a system in this called slug handlers - https://github.com/doctrine... - but i've never used them and I'm not sure if they will accomplish what you're looking for. But, it's worth a try. It looks like you need to:

A) make a class that implements SlugHandlerInterface: https://github.com/doctrine...
B) Configure this on your slug field - https://github.com/doctrine...
C) Finally, in your "slug handler" class, I think you need to do nothing in each method EXCEPT for onSlugCompletion. In this method, use the $object variable - which will be your entity object - and check $slugManual. If you do NOT want a slug generated, set the $slug argument to null. That argument is passed "by reference" so simply setting it to null will change that variable where it's used... and I think that it will cause the $slug to be set to null.

So... give it a try and let me know if it works!

Cheers!

Reply
Jayant B. Avatar
Jayant B. Avatar Jayant B. | posted 1 year ago

Hello

I have tried the updated as well but still i am getting following errors -

C:\wamp64\www\Symfony001\Symfony-Doctrine>composer require stof/doctrine-extensions-bundle
Using version ^1.6 for stof/doctrine-extensions-bundle
./composer.json has been updated
Running composer update stof/doctrine-extensions-bundle
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

Problem 1
- Root composer.json requires stof/doctrine-extensions-bundle ^1.6 -> satisfiable by stof/doctrine-extensions-bundle[v1.6.0].
- stof/doctrine-extensions-bundle v1.6.0 requires symfony/config ^4.4 || ^5.2 -> found symfony/config[v4.4.0, ..., v4.4.30, v5.2.0, ..., v5.3.4] but the package is fixed to v5.1.11 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.

Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.

Installation failed, reverting ./composer.json and ./composer.lock to their original content.

what else i am missing or can try?

thanks

Reply

Hey Jayant,

Did you try to the solution from the error message? I.e. try to run that command with --with-all-dependencies option:

$ composer require stof/doctrine-extensions-bundle --with-all-dependencies

This should help. Otherwise, requiring a lower version of stof/doctrine-extensions-bundle may help to, you can specify a specific version like this:

$ composer require "stof/doctrine-extensions-bundle:^1.5"

Btw, we use v1.5.0 in this course, so this should definitely work :)

Cheers!

Reply
Sherri Avatar

Installing the Stof extensions bundle gives errors because you need at least Symfony 5.2 and this project is on 5.1.

Upgrade Symfony first, here is the docs for that: https://symfony.com/doc/cur...

Reply

Hey Sherri,

Yeah, the latest v1.6.0 of the bundle requires Symfony 5.2, but you can install the previous one v1.5.0 instead that fits for ^5.0. You can do it:

$ composer require "stof/doctrine-extensions-bundle:^1.5.0"

Cheers!

1 Reply
Covi A. Avatar
Covi A. Avatar Covi A. | posted 1 year ago

Hi, i saw the same problem like Aaroon kincer
the problem is


composer require stof/doctrine-extensions-bundle

Using version ^1.6 for stof/doctrine-extensions-bundle
./composer.json has been updated
Running composer update stof/doctrine-extensions-bundle
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

Problem 1
- Root composer.json requires stof/doctrine-extensions-bundle ^1.6 -> satisfiable by stof/doctrine-extensions-bundle[v1.6.0].
- stof/doctrine-extensions-bundle v1.6.0 requires symfony/config ^4.4 || ^5.2 -> found symfony/config[v4.4.0, ..., v4.4.22, v5.2.0, ..., v5.2.7] but the package is fixed to v5.1.2 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.

Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.

Installation failed, reverting ./composer.json and ./composer.lock to their original content.

If i have to upgrade symfony version then could you please tell me how can i upgrade it.

I am using symfony 5.1.2 version.

By the way thanks for this helpful tutorial.

Reply

Hey Monoranjan,

Yeah, that's because your Symfony version is 5.1... you should upgrade it to 5.2 first and then you should be able to install that latest 1.6 version of stof/doctrine-extensions-bundle . For Symfony 5.1 you can only install v1.5.0 of stof/doctrine-extensions-bundle that allows ^5.0 for Symfony components, but if you want the latest v1.6 - the only way to install it - upgrade Symfony to 5.2. Thankfully, it should be a minor easy upgrade.

Cheers!

Reply
Jörg daniel F. Avatar
Jörg daniel F. Avatar Jörg daniel F. | posted 1 year ago

Hello, I got this error message while intel the stop doctrine. can someone help me please? my head will explode in a minute :-(

➜ sites git:(chapter01) ✗ composer require doctrine/common:^2.7 --update-with-dependencies./composer.json has been updated
Running composer update doctrine/common --with-dependencies
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- doctrine/orm is locked to version 2.8.3 and an update of this package was not requested.
- doctrine/orm 2.8.3 requires doctrine/common ^3.0.3 -> found doctrine/common[3.0.3, 3.1.0, 3.1.1, 3.1.2] but it conflicts with your root composer.json require (^2.7).
Problem 2
- doctrine/orm 2.8.3 requires doctrine/common ^3.0.3 -> found doctrine/common[3.0.3, 3.1.0, 3.1.1, 3.1.2] but it conflicts with your root composer.json require (^2.7).
- doctrine/doctrine-fixtures-bundle 3.4.0 requires doctrine/orm ^2.6.0 -> satisfiable by doctrine/orm[2.8.3].
- doctrine/doctrine-fixtures-bundle is locked to version 3.4.0 and an update of this package was not requested.
Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.
Installation failed, reverting ./composer.json and ./composer.lock to their original content.
Reply

Hey Jorg,

Hm, looks like you have doctrine/orm installed that is locked on 2.8.3 and that requires to have doctrine/common 3.0.3 or higher. To install the doctrine/common:^2.7 you should either downgrade the doctrine/orm to ^2.7... or require "doctrine/common^2.8.3" at least that will fit doctrine/orm version you have installed.

I hope this helps!

Cheers!

1 Reply
Jörg daniel F. Avatar
Jörg daniel F. Avatar Jörg daniel F. | victor | posted 1 year ago

Hello Victor,

yes help so that I have an new error :-D

Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires stof/doctrine-extensions-bundle ^1.6 -> satisfiable by stof/doctrine-extensions-bundle[v1.6.0].
- stof/doctrine-extensions-bundle v1.6.0 requires symfony/config ^4.4 || ^5.2 -> found symfony/config[v4.4.0, ..., v4.4.20, v5.2.0, ..., v5.2.4] but these were not loaded, likely because it conflicts with another require.
Installation failed, reverting ./composer.json and ./composer.lock to their original content.
Reply

Hey Jorg,

Hm, could you tell me what command exactly do you run? :) Btw, what Symfony version do you have? Looks like the stof/doctrine-extensions-bundle^1.6 requires symfony/config ^4.4 || ^5.2 - if you have a lower version, you would probably need to upgrade symfony/config first.

I hope this helps!

Cheers!

1 Reply
Jörg daniel F. Avatar
Jörg daniel F. Avatar Jörg daniel F. | victor | posted 1 year ago
composer require stof/doctrine-extensions-bundle --with-all-dependencies
Using version ^1.6 for stof/doctrine-extensions-bundle
./composer.json has been updated
Running composer update stof/doctrine-extensions-bundle --with-all-dependencies
Loading composer repositories with package information
Restricting packages listed in "symfony/symfony" to "5.1.*"
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires stof/doctrine-extensions-bundle ^1.6 -> satisfiable by stof/doctrine-extensions-bundle[v1.6.0].
- stof/doctrine-extensions-bundle v1.6.0 requires symfony/config ^4.4 || ^5.2 -> found symfony/config[v4.4.0, ..., v4.4.20, v5.2.0, ..., v5.2.4] but it conflicts with your root composer.json require (5.1.*).
Installation failed, reverting ./composer.json and ./composer.lock to their original content.
Reply

Hey Jörg daniel F.

I'm afraid that Victor is right and you'll have to upgrade Symfony5.1 to Symfony5.2 in order to install that bundle

Reply
Jörg daniel F. Avatar
Jörg daniel F. Avatar Jörg daniel F. | MolloKhan | posted 1 year ago

Hi Folks,

i solved my problem with this site

https://symfony.com/doc/cur...

with this I make a Symfony upgrade to 5.2 and voila it works.

Thx @ all for your help

1 Reply
Will T. Avatar
Will T. Avatar Will T. | posted 1 year ago

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'slug' cannot be null

I'm still getting this error afterupdating the QuestionFactory and adding @Gedmo\Slug(fields={"name"})

any idea what could be wrong? I had to create the stof_doctrine_extensions.yaml file manually as stof/doctrine-extensions-bundle didn't install with the options as in the lesson.

Reply

Hey Will T.!

Hmmm. This definitely *feels* like the feature is simply not working at all. You mentioned that the stof_doctrine_extensions.yaml file was not added for you. This IS part of the recipe for this bundle - https://github.com/symfony/...

This make me think that the recipe did *not* execute. Possible reasons are that (A) you've disabled "contrib" recipes in your composer.json file - the extra.symfony.allow-contrib key - or (B) you answered "No" when it asked you if you wanted to install it or (C) you're not using Flex (or D, something weird just happened, that's always possible).

*Anyways*, good job adding the stof_doctrine_extensions.yaml file manually. But also check that the bundle is enabled inside of config/bundles.php. That is one other possible thing that's going wrong.

Overall, the process "should" be pretty simple:

A) The bundle must be enabled
B) If the sluggable behavior is enabled in your config file - https://symfonycasts.com/sc... - then the "SluggableListener" will be activated
C) Then, as long as you have the @Gedmo\Slug (which you do), then the magic will happen

If you were missing (A) or (B), then it would silently not work. Any other problems - like types in the annotation, etc - should result in a clear exception.

Let me know if you find out anything! Also, though it's rarely needed, when super weird things happen, you can try to run bin/console cache:clear.

Cheers!

Reply
Will T. Avatar

Hey Ryan!

I fixed this myself after looking through the bundles.php directory.
I found that Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true] was missing, so I added it.
Thanks for your thorough answer, though :)

Reply

Hey Will T.!

Nice debugging and I'm just glad you got it working :).

Cheers!

Reply
Daniel K. Avatar
Daniel K. Avatar Daniel K. | posted 2 years ago

It would be great if you showed how to use translatable and sluggable together!
e.g. a product has a "name", this is usually used as a slug in the frontend
If you have a translated "name", you also want a translated "slug".
and in the frontend you would like to load the product data with a url like this (example):
/{_locale}/product/{slug}

DoctrineExtension does a good job. I just think that the parameter converter needs some special tweeking that might be quite interesting!

Reply

Hey Daniel K. !

Hmm, cool suggestion!

> DoctrineExtension does a good job. I just think that the parameter converter needs some special tweeking that might be quite interesting!

Yes, I think you're right about this... and I think you would need a custom param converter to do the job. Well, you could add @ParamConverter annotations, but that's annoying ;). Well, actually, I would create a custom "argument resolver" - not a param converter, just because that whole system was written before the "argument resolver" system, which is now much easier. We talk about that topic in general in the deep dive course: https://symfonycasts.com/sc...

Making an argument resolver work would be *fairly* easy - you could, for example, look specifically to see if there is a $request->attributes->get('slug') and, if there is, use $request->getLocale() and that value to query for the entity. The ArgumentMetadata $argument that's passed to your custom argument value resolver contains the type-hinted class name for each argument via ->getType(). You could use that in supports() to know if your argument resolver should operate on an argument (easiest way would be to have a list of classes you want to support, harder would be to check to see if the class is an entity in Doctrine or not) and then use it again in resolve to fetch the repository and make the query.

That's a quick answer to a not-so-easy task. If you're interested, let me know if you need more details :).

Cheers!

Reply
Daniel K. Avatar

In the "resolve" part: you query for the object. What if there is none. e.g. "getOneOrNullResult" returns "null"?

How do you throw a 404?
if the resolver yields a "null" value a 500 server error is thrown, because the controller expects an instance of the entity
So would you throw a NotFoundHttpException right in the Argument Resolver?

Reply

Hey Daniel K.

Yes, as far as I know that's what you have to do to trigger a 404 page. It's something similar as when you type-hint for an entity argument in your controller's method, it will query it for you or throw a `NotFoundHttpException` which will trigger a 404 page

Cheers!

Reply
Daniel K. Avatar

Thank you for this hint and the great idea! I tried it and it seems to me that the ParamConverter kicks in before the argumentValueResolver.So the "auto_convert" option of the param converter needs to be set to false. But of course i'd like to use it for other requests :)
Any thoughts on this?

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^2.1", // 2.1.1
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.7", // 2.8.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "sentry/sentry-symfony": "^4.0", // 4.0.3
        "stof/doctrine-extensions-bundle": "^1.4", // v1.5.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.17.5
        "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.8.0
        "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.4.0
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.23.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.5.0
    }
}