Sluggable & other Wonderful Behaviors

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

We're using Faker to generate a random slug for each dummy article. Thanks to this, back on the homepage, look at the URLs: they're truly random slugs: they have no relation to the title.

But, really, shouldn't the slug be generated from the title? What I mean is, if I set the Article's title, something should automatically convert that into a slug and make sure it's unique in the database. We shouldn't need to worry about doing that manually.

And... yea! There's a really cool library that can do this, and a bunch of other magic! Google for StofDoctrineExtensionsBundle, and then click into its documentation.

Ok, let me explain something: there is a normal, PHP library called DoctrineExtension, which can add a lot of different behaviors to your entities, like sluggable, where you automatically generate the slug from another field. Other behaviors include Loggable, where each change to an entity is tracked, or Blameable, where the user who created or updated an entity is automatically recorded.

Installing StofDoctrineExtensionsBundle

This bundle - StofDoctrineExtensionsBundle - helps to integrate that library into a Symfony project. Copy the composer require line, find your terminal, and paste!

Tip

stof/doctrine-extensions-bundle is now compatible with Symfony 5 - you can use it instead of antishov/doctrine-extensions-bundle fork

composer require antishov/doctrine-extensions-bundle

While that's working, let's go check out the documentation. This is a wonderful library, but its documentation is confusing. So, let's navigate to the parts we need. Scroll down to find a section called "Activate the extensions you want".

As we saw, there are a lot of different, possible behaviors. For performance reasons, when you install this bundle, you need to explicitly say which behaviors you want, like timestampable, by setting it to true.

Contrib Recipes

Move back to the terminal to see if things are done. Oh, interesting! It stopped! And it's asking us if we want to install the recipe for StofDoctrineExtensionsBundle. Hmm... that's weird... because Flex has already installed many other recipes without asking us a question like this.

But! It says that the recipe for this package comes from the "contrib" repository, which is open to community contributions. Symfony has two recipe repositories. The main repository is closely controlled for quality. The second - the "contrib" repository - has some basic checks, but the community can freely contribute recipes. For security reasons, when you download a package that installs a recipe from that repository, it will ask you first before installing it. And, there's a link if you want to review the recipe.

I'm going to say yes, permanently. Now the recipe installs.

Configuring Sluggable

Thanks to this, we now have a shiny new config/packages/stof_doctrine_extensions.yaml file:

# 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

This is where we need to enable the extensions we want. We want sluggable. We can use the example in the docs as a guide. Add orm, then default. The default is referring to the default entity manager... because some projects can actually have multiple entity managers. Then, sluggable: true:

# 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
orm:
default:
sluggable: true

As soon as we do this... drumroll... absolutely nothing will happen. Ok, behind the scenes, the bundle is now looking for slug fields on our entities. But, we need a little bit more config to activate it for Article. Open that class and find the slug property.

Now, go back to the documentation. Another confusing thing about this bundle is that the documentation is split in two places: this page shows you how to configure the bundle... but most of the docs are in the library. Scroll up and find the DoctrineExtensions Documentation link.

Awesome. Click into sluggable.md. Down a bit... it tells us that to use this feature, we need to add an @Gedmo\Slug() annotation above the slug field. Let's do it! Use @Gedmo\Slug, then fields={"title"}:

... lines 1 - 5
use Gedmo\Mapping\Annotation as Gedmo;
... lines 7 - 10
class Article
{
... lines 13 - 24
/**
... line 26
* @Gedmo\Slug(fields={"title"})
*/
private $slug;
... lines 30 - 154
}

That's all we need! Back in ArticleFixtures, we no longer need to set the slug manually (remove it):

... lines 1 - 7
class ArticleFixtures extends BaseFixture
{
... lines 10 - 26
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) {
$article->setTitle($this->faker->randomElement(self::$articleTitles))
->setSlug($this->faker->slug)
... lines 32 - 49
);
... lines 51 - 60
});
... lines 62 - 63
}
}

Try it out: find your terminal, and load those fixtures!

php bin/console doctrine:fixtures:load

No errors! That's a really good sign, because the slug column is required in the database. Go back to the homepage and... refresh! Brilliant! The slug is clean and clearly based off of the title! As an added benefit, look at how some of these have a number on the end. The Sluggable behavior is making sure that each slug is unique. So, if a slug already exists in the database, it adds a -1 , -2, -3, etc. until it finds an open space.

Hello Doctrine Events

Side note: this feature is built on top of Doctrine's event system. Google for "Doctrine Event Subscriber". You'll find a page on the Symfony documentation that talks about this very important topic. We're not going to create our own event subscriber, but it's a really powerful idea. In this example, they talk about how you could use the event system to automatically update a search index, each time any entity is created or updated. Behind the scenes, the sluggable features works by adding an event listener that is called right before saving, or "flushing", any entity.

If you ever need to do something automatically when an entity is added, updated or removed, think of this system.

Next, let's find out how to rescue things when migrations go wrong!

Leave a comment!

  • 2020-06-12 Diego Aguiar

    That's great! Thanks for the heads up Denis Žoljom

  • 2020-06-12 Denis Žoljom

    Seems like `StofDoctrineExtensionsBundle` 1.4.0 supports Symfony 5: https://github.com/stof/Sto...

  • 2020-03-03 weaverryan

    Hey JP Fortuno!

    Thanks for catching that! When we only "remove" a line... it's a tough thing to show in the code block... but I think we should at least try by showing the new code but with the line missing. We'll add a code block.

    Cheers!

  • 2020-02-29 JP Fortuno

    Not sure about this but one important line which is missing in the Course Script but present in the video is the removal of the line

    ->setSlug($this->faker->slug)

    from the loadData function in the ArticleFixtures.php class. Hope that helps

  • 2020-01-21 Victor Bocharsky

    Hey Ed,

    Ah, sure, then it fits perfect in this case! I did the same if I would need a quick hack to follow the tutorial I think ;)

    Cheers!

  • 2020-01-20 Ed Barnard

    Victor, I completely agree on the random number bit. That was a quick hack to get through the tutorial - and it was a good excuse to see if a virtual field works as I was expecting. It does!

  • 2020-01-20 Victor Bocharsky

    Hey Ed,

    Thank you for sharing your solution of using KnpLabs/DoctrineBehaviors bundle. I'm not a big fan of random number in slugs, but probably it's the easiest and quickest solution to make slugs unique with this bundle.

    Cheers!

  • 2020-01-18 Ed Barnard

    The bundle is working well for sluggable and timestampable chapters here, with Symfony 5. I needed to add to config/services.yaml:


    Knp\DoctrineBehaviors\EventSubscriber\SluggableSubscriber:
    tags:
    - { name: 'doctrine.event_subscriber' }

    Knp\DoctrineBehaviors\EventSubscriber\TimestampableSubscriber:
    tags:
    - { name: 'doctrine.event_subscriber' }

    Knp\DoctrineBehaviors\Repository\DefaultSluggableRepository:
    class: Knp\DoctrineBehaviors\Repository\DefaultSluggableRepository

    For the article class:


    class Article implements SluggableInterface, TimestampableInterface
    {
    use SluggableTrait;
    use TimestampableTrait;

    /**
    * @return string[]
    */
    public function getSluggableFields(): array
    {
    return ['title', 'rand'];
    }

    /**
    * @return string
    * @throws Exception
    */
    public function getRand(): string
    {
    return (string)random_int(1000000, 9999999);
    }
  • 2020-01-03 Victor Bocharsky

    Hey Tomas,

    Yay! Thank you for all your huge job in that bundle! That's GREAT it supports Symfony 5 now :)

    Cheers!

  • 2020-01-03 Tomáš Votruba

    Doctrine Behaviors 2 is out!

    https://www.tomasvotruba.cz...

  • 2019-12-30 Tomáš Votruba

    Cheers :)

    The migration path is half-ready: https://github.com/rectorph...
    Now it needs testing in user-land.

    I'll write about it in next 1-2 weeks

  • 2019-12-30 Victor Bocharsky

    Hey Tomas,

    Thank you for sharing your thoughts about it. And good to know that Rector could help with some migrations if needed!

    Cheers!

  • 2019-12-27 Tomáš Votruba

    Hey,

    it's Github info, as you can see yourself. I don't know any mantainers who would be on time to tell people "the package is dead" even if everyone can see it for last 2 years. We miss this honesty in open-source, though it would help lot of project to re-enter legacy.

    If you have any troubles with DoctrineBehaviors, just create an issue on Github.

    Btw, Rector migration from gedmo/stof to KnpLabs is more than 50 % ready: https://github.com/rectorph...

  • 2019-12-27 Victor Bocharsky

    Hey Tomas,

    Really sad to hear it! Is it official info? Any links that officially confirm they are abandoned except the fact they are *very* slow on pushing things forward?

    And thanks for sharing the link to KnpLabs/DoctrineBehaviors - it's a really good alternative IMO.

    Cheers!

  • 2019-12-25 Tomáš Votruba

    gedmo/stof extensions are dead since 2017.
    This will help: https://github.com/KnpLabs/...

  • 2019-08-20 Diego Aguiar

    Ohh, in that case you can open an issue, or even better send a pull request to the vendor's project :)

    BTW, it won't affect you until Symfony 5, so you can just forget about it for awhile

    Cheers!

  • 2019-08-20 Ozornick

    IYes, I tried to search. But the error is in the vendor/ package. Is it undesirable to change it?

    namespace Stof\DoctrineExtensionsBundle\DependencyInjection;

    public function getConfigTreeBuilder()
    {
    $treeBuilder = new TreeBuilder();
    $rootNode = $treeBuilder->root('stof_doctrine_extensions');
    ...

  • 2019-08-19 Diego Aguiar

    Hey Ozornick

    Yes, we added a note about that deprecation here: https://symfonycasts.com/sc...

    Cheers!

  • 2019-08-19 Ozornick

    I have the same problems. 20 posts deprecated. "A tree builder without a root node is deprecated since Symfony 4.2 and will not be supported anymore in 5.0."

  • 2019-07-15 Diego Aguiar

    Hey Neal Ostrander

    It looks like a permissions problem. Try changing the permissions of that folder and try again.

    Cheers!

  • 2019-07-13 Neal Ostrander

    Working on Windows 10 after reloading the doctrine fixtures in the data base when I refresh the home page the php server dies with the following error:

    Compile Error: ContainerNB37shz\srcDevDebugProjectContainer::load(): Failed opening required 'C:\Projects\the_spacebar\var\cache\dev\ContainerNB37shz\getConsole_ErrorListenerService.php' (include_path='C:\xampp\php\PEAR')

    any help on figuring out what is happening would be appreciated.

  • 2019-06-11 Christopher

    Awesome! Thanks for looking into that for us, Ryan.

  • 2019-06-11 weaverryan

    Ah, indeed! It looks likely that the tag is the issue! Hopefully we can get a tag soon :)

  • 2019-06-05 Christopher

    Hey Ryan!

    Thanks for the reply. Yep. I've done all those things as well. I even deleted the composer.lock file, made sure that I had the latest version in my composer.json file ("stof/doctrine-extensions-bundle": "^1.3"), and then ran composer install once again. Still had all the same deprecation warnings plus many more as a result of upgrading to Symfony 4.3.

    I'm curious if this is playing a part in the whole ordeal - https://github.com/stof/Sto.... Perhaps we're just dealing with a tag issue?

    Thanks so much for any feedback, Ryan!

  • 2019-06-05 weaverryan

    Hey Christopher!

    Try running composer update. The version in your symfony.lock file isn't actually the version you have installed - it's just the version of the "recipe" that was originally used when it was first installed. As you noted, the *true* version you have installed can be found in composer.lock (or via composer show stof/doctrine-extensions-bundle. However, running composer install doesn't update the composer.lock file - it just reinstalls the version that are listed in that file. To force it to actually download the *newest* version of a library, use composer update.

    Let us know if this helps!

    Cheers!

  • 2019-06-04 Christopher
  • 2019-06-04 Christopher

    I used this thread as a reference, deleted my vendor directory, removed the reference from the symfony.lock file, and finally ran composer install from command line. Funny enough, the composer.lock file still fails to update to version 1.3 and I still get all the same deprecation warnings.

  • 2019-06-04 Christopher

    Sorry, the stof_doctrine_extensions.yaml config spaces were removed by the browser. They're indented on my end though.

  • 2019-06-04 Christopher

    Thanks for the reply, Vladimir

    The message is seen in the profiler logs. I have been following the tutorials, one after another, but perhaps I've missed something at some point. If it helps, I'm using this in composer.json (which appears to be the latest version on packagist):

    "stof/doctrine-extensions-bundle": "^1.3"

    Then in stof_doctrine_extensions.yaml:

    stof_doctrine_extensions:
    default_locale: en_US
    orm:
    default:
    sluggable: true
    timestampable: true

    Would there be another location that config is necessary?

    **UPDATE**
    I just checked my symfony.lock file and for whatever reason, it's using 1.2. I'll try fixing things from that file as well.

  • 2019-06-04 Vladimir Sadicov

    Hey @Christopher

    It's caused by outdated stof/doctrine-extensions-bundle. Where have you got this deprecation message? Is it in our course code? Have you followed course from the start? or playing with finish directory?

    Cheers!

  • 2019-06-04 Christopher

    Hey guys! Was curious if someone could shed some light on the following deprecation warning that I'm seeing in the profiler logs:

    A tree builder without a root node is deprecated since Symfony 4.2 and will not be supported anymore in 5.0.

    It looks like this is/was being dealt with here and that a fix was merged in for the symfony/maker-bundle, but the trace shows the warning starting in .../vendor/stof/doctrine-extensions-bundle/DependencyInjection/Configuration.php:17. When it's all said and done, it looks like 10 deprecation messages are logged.

    Any idea what might be going on? Anything I can do to fix the deprecations, or will this be fixed in 4.3?

    Thanks guys! Loving the well-constructed tutorials!

  • 2019-04-29 Victor Bocharsky

    Hey Mike,

    Tree strategy is tricky, so probably it's a good idea to use a good already-written-and-tested implementation of it instead of doing it on your own, but it depends on what features do you need. If you don't need all the features from Tree behavior - it might be enough to use just DB "One-To-Many Self-referencing" relations and have getParent()/getChildren() methods, see https://www.doctrine-projec... . But if you need more complex solution, take a look at Tree implementation from Gedmo - it looks really powerful with many features and examples. KnpLabs' one should be good as well, but what about me I'd probably choose Gedmo one for this strategy, at least it has good docs with many examples.

    Cheers!

  • 2019-04-29 Victor Bocharsky

    Hey Mike,

    Thank you for sharing your solution with others.

    Cheers!

  • 2019-04-26 Mike

    Could I ask you one more question?
    For categories, do you use the Tree component of Stof Doctrine Extensions of do you do it on "your own"?
    What do you suggest to implement categories inside a web project? (By example Categories for space bar articles)

  • 2019-04-26 Mike

    Thank you Ryan, for serving us with your wisdom :)
    To honour your time, here is my final working result, it will may help someone:

    Custom Listener (Has to be registered inside stof config.yml as sluggable class):

    class SluggableListener extends GedmoSluggableListener {

    public function __construct(){
    $this->setTransliterator(array('\App\Listener\SluggableListener', 'transliterate'));
    }

    /**
    * since transliterate will convert "ä" to an "a", i added this hack to call urlize first so it is converted to "ae" first
    *
    * @param string $text
    * @param string $separator
    * @return string $text
    */
    public static function transliterate($text, $separator = '-')
    {
    $text = Urlizer::urlize($text, $separator);
    return Urlizer::transliterate($text, $separator);
    }

    }

    Entity:

        public function setSlug(string $slug): self
    {
    // Instead of using @ Gedmo|Sluggable as annotation, manually convert the $slug to a Slug
    // (Because Gedmo|Sluggable requires to be binded to another field. But the Slug is unique / not binded.)
    $this->slug = SluggableListener::transliterate($slug);

    return $this;
    }

  • 2019-04-19 weaverryan

    Hey Mike!

    Hmm, I don't know the answer for sure, but by the information you're giving, I think you've clearly investigated it and found out that this is probably not possible. A quick search didn't turn up any possibilities. Victor from SfCasts mentioned that this might be possible with https://github.com/KnpLabs/... - but I'm not sure.

    Overall, though, it seems like if you need to generate the slug value coming completely from an external source... I'd just do it manually. The class that's responsible for transforming the string into a URL-safe string is here: https://github.com/Atlantic... - so you can use that manually to do it. You could *even* do this in a Doctrine listener, if you wanted it to be automatic.

    Let me know if this makes sense! It sounds like it might be a situation where what you need is different enough that using a library is making your life *harder* instead of easier.

    Cheers!

  • 2019-04-18 Mike

    Is it possible to use sluggable without a field it is maped by?
    I need a custom slug which is not connected to any field inside the DB.

    But after over 1 hour googling and trial&error the best I came up with, is to create a $slugPlaceholder column which holds the string that gets converted into $slug.
    But it seems to be "overhead" for my use case, because $slugPlaceholder is not really needed as column.

    I've tried to use temporary $slugPlaceholder variable, but the 'fields' annotation of slugable seems to only accept "real" columns?!
    Is there any alternative to get rid of $slugPlaceholder and use only $slug?

    Current code:

    /**
    * slugPlaceholder which is to become $slug
    * @ORM\Column(type="string", length=255, unique=true)
    */
    private $slugPlaceholder;

    /**
    * @ORM\Column(type="string", length=255, unique=true)
    * @Gedmo\Slug(fields={"slugPlaceholder"})
    */
    private $slug;
    ...
    public function getSlugPlaceholder(): ?string
    {
    return $this->slugPlaceholder;
    }

    // Gets automatically converted to $slug via sluggable
    public function setSlug(string $slug): self
    {
    $this->slugPlaceholder = $slug;

    return $this;
    }

    public function getSlug(): ?string
    {
    return $this->slug;
    }

  • 2019-02-25 Manoj Kumar

    Hi, yes now it works perfectly. Most probably it was a cache issue on windows system. But works great on Linux.
    Thanks so much

  • 2019-02-25 weaverryan

    Hey Manoj Kumar!

    Hmm. I just tried the latest commit on your repository - and it looks like it's working fine now - no errors when I load the fixtures and the slug is being set correctly. Are you till having problems?

    Cheers!

  • 2019-02-24 Manoj Kumar

    Hi, I have followed exactly same steps, but somehow sluggable is not working. Here is commit related to that: https://github.com/napester...
    I am getting this error:
    In PDOStatement.php line 119:

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


    In PDOStatement.php line 117:

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

  • 2018-11-08 weaverryan

    Hi mouad err!

    You should be able to the @Blameable to do that automatically - http://atlantic18.github.io...

    Just make sure to remember to enable the "blameable" in the stof_doctrine_extensions.yaml file, just like we did with timestampable here: https://symfonycasts.com/sc...

    Cheers!

  • 2018-11-08 mouad err

    i have 2 columns CreatedBy and UpdatedBy, how i can automatically fill them with the current user id when create or update entity?
    thanks in advance.

  • 2018-11-05 weaverryan

    Hey AbelardoLG! Nice job figuring it out - sorry we confused you for a moment!

  • 2018-11-03 AbelardoLG

    Myself answer: I didn't watched the video where you delete the fake slug. This part is not mentioned in your documentation.

    Cheers. :)

  • 2018-11-03 AbelardoLG

    Hello there!

    I followed your steps by installing Gedmo library but the slug (link) is not similar to the title of the article.

    What was wrong?

  • 2018-10-08 Victor Bocharsky

    Hey Dmitriy,

    Good work! And thank you for sharing your code with others. Just a few minor tips:
    - findAll() will find you all Card entities whether with or without slug, but you probably want to proceed only those entities that do not have slugs here, but probably not, depends on your needs :)
    - And right now your class is not in sync with command name, I'd recommend to rename it to something like "UpdateCardSlugsCommand"

    Cheers!

  • 2018-10-05 Дмитрий Ченгаев

    Thank you. I did it. Here is my code:


    namespace App\Command;

    use App\Entity\Card;
    use Doctrine\ORM\EntityManagerInterface;
    use Symfony\Component\Console\Command\Command;
    use Symfony\Component\Console\Input\InputInterface;
    use Symfony\Component\Console\Output\OutputInterface;

    class CreateUserCommand extends Command
    {
    /**
    * @var EntityManagerInterface
    */
    private $em;


    /**
    * CreateUserCommand constructor.
    */
    public function __construct(EntityManagerInterface $em) {

    $this->em = $em;
    parent::__construct();
    }

    protected function configure()
    {

    $this
    // the name of the command (the part after "bin/console")
    ->setName('app:update-card-slug')
    ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {

    foreach ($this->em->getRepository(Card::class)->findAll() as $entity) {
    $entity->setSlug(null);
    }

    $this->em->flush();
    $this->em->clear();

    $output->writeln('Done.');
    }
    }
  • 2018-10-05 Victor Bocharsky

    Hey Dmitriy,

    Yes! That's exactly what you need, nice catch on Symfony docs :) Sorry, just saw this message, though, I already replied to you here: https://symfonycasts.com/sc...

    Cheers!

  • 2018-10-05 Victor Bocharsky

    Hey Dmitriy,

    Sure, you can take a look at docs: https://symfony.com/doc/cur... - just create a simple Symfony command as is shown there. And inside of execute() write the code we're talked about: fetch ALL entities that do not have slug, generate a new slug (but most probably you just need to update the updatedAt field to those entities so Doctrine will think that those entities are changed and called the listener on them that will actually generate slugs), and then just call flush(). As you can guess, you'll need Doctrine entity manager in this command, take at this part: https://symfony.com/doc/cur... - to figure out how to inject it. And fo course, just try this command locally first to make sure it works. Then, deploy it to production and run there.

    Cheers!

  • 2018-10-04 Дмитрий Ченгаев

    Maybe this is something like this?

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

  • 2018-10-04 Дмитрий Ченгаев

    Thank you, Victor. Maybe somewhere have a small example of how to create such console command? I never did that. Unfortunately, this is not clear to me.

  • 2018-10-04 Victor Bocharsky

    Hey Dmitriy,

    Good question! Since slug generated with a listener, it’s not possible to do in doctrine migration. So, you need a so called console migration. It’s a simple Symfony console command where you fetch all entities without slug and iterate over them changing updatedAt field in order to set entities in a dirty state. Then call flush() and listener will do the restaurant or you and generate slugs.

    But, after you execute the command, it’s a good idea to write a doctrine migration to make the slug field not nullable.

    Cheers!

  • 2018-10-04 Дмитрий Ченгаев

    How to add slug for existing records in the database? I added a slug property for an entity, but for all existing records its value is NULL.