Buy
Buy

Sluggable & other Wonderful Behaviors

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!

composer require stof/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. 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!

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