Buy

Since slug is just a normal field, we could open our fixtures file and add the slug manually here to set it:

37 lines src/AppBundle/DataFixtures/ORM/fixtures.yml
AppBundle\Entity\Genus:
genus_{1..10}:
name: <genus()>
... lines 4 - 37

LAME! There's a cooler way: what if it were automagically generated from the name? That would be awesome! Let's go find some magic!

Installing StofDoctrineExtensionsBundle

Google for a library called StofDoctrineExtensionsBundle. You can find its docs on Symfony.com. First, copy the composer require line and paste it into your terminal:

Tip

If you are on Symfony 3.2 or higher, you don't have to specify the bundle's version

composer require stof/doctrine-extensions-bundle:1.2

Second, plug the bundle into your AppKernel: copy the new bundle statement, open app/AppKernel.php and paste it here:

58 lines app/AppKernel.php
... lines 1 - 5
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
... lines 11 - 21
new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
... lines 23 - 24
);
... lines 26 - 35
}
... lines 37 - 56
}

And finally, the bundle needs a little bit of configuration. But, the docs are kind of a bummer: it has a lot of not-so-important stuff near the top. It's like a treasure hunt! Hunt for a golden cold block near the bottom that shows some timestampable config.yml code. Copy this. Then, find our config.yml file and paste it at the bottom. And actually, the only thing we need is under the orm.default key: add sluggable: true:

81 lines app/config/config.yml
... lines 1 - 75
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
sluggable: true

This library adds several different magic behaviors to Doctrine, and sluggable - the automatic generation of a slug - is just one of them. And instead of turning on all the magic features by default, you need to activate the ones that you want. That's actually pretty nice. Another great behavior is Timestampable: an easy way to add createdAt and updatedAt fields to any entity.

The DoctrineExtensions Library

Head back to the documentation and scroll up. Near the top, find the link called DoctrineExtensions documentation and click it.

The truth is, StofDoctrineExtensionsBundle is just a small wrapper around this DoctrineExtensions library. And that means that most of the documentation also lives here. Open up the Sluggable documentation, and find the code example.

Adding the Sluggable Behavior

Ok cool, this is easy. Copy the Gedmo use statement above the entity: it's needed for the annotation we're about to add. Open Genus and paste it there:

168 lines src/AppBundle/Entity/Genus.php
... lines 1 - 7
use Gedmo\Mapping\Annotation as Gedmo;
... lines 9 - 168

Then, above the slug field, we'll add this @Gedmo\Slug annotation. Just change fields to simply name:

168 lines src/AppBundle/Entity/Genus.php
... lines 1 - 7
use Gedmo\Mapping\Annotation as Gedmo;
... lines 9 - 14
class Genus
{
... lines 17 - 29
/**
* @ORM\Column(type="string", unique=true)
* @Gedmo\Slug(fields={"name"})
*/
private $slug;
... lines 35 - 166
}

That is it! Now, when we save a Genus, the library will automatically generate a unique slug from the name. And that means we can be lazy and never worry about setting this field ourselves. Nice.

Reload the Fixtures

Head back to your terminal. Woh! My composer require blew up! But look closely: the library did install, but then it errored out when it tried to clear the cache. This is no big deal, and was just bad luck: I was right in the middle of adding the config.yml code when the cache cleared. If I run composer install, everything is happy.

Now, because our fixtures file sets the name property, we should just be able to reload our fixtures and watch the magic:

./bin/console doctrine:fixtures:load

So far so good. Let's check the database. I'll use the doctrine:query:sql command:

./bin/console doctrine:query:sql 'SELECT * FROM genus'

Got it! The name is Balaena and the slug is the lower-cased version of that. Oh, and at the bottom, one of the slugs is trichechus-1. There are two genuses with this name. Fortunately, the Sluggable behavior guarantees that the slugs stay unique by adding -1, -2, -3 etc when it needs to.

So the slug magic is all done. Now we just need to update our app to use it in the URLs.

Leave a comment!

  • 2018-08-16 weaverryan

    Hey daro_G!

    Hmm. So, in Symfony 4, we take advantage of a lot of autowiring, which is really great, except that if you try to use some older code on Symfony 4, some things will need to change to get everything working :). If you've copied the code from this project into a Symfony 4 project (including copying the services.yml file into services.yaml) and you are getting this error, then the cause is probably that you need to remove these lines from your new services.yaml file: https://github.com/symfony/...

    If you remove those lines, then you are effectively using the "Symfony 3" coding style inside a Symfony 4 project. If you're serious about learning Symfony 4, I'd definitely recommend checking into the new autowiring stuff - we cover it all in our Symfony 4 series. If you just want to learn more about the specific topics in *this* tutorial, then I might recommend just coding through this tutorial using the code download on this page (so, in Symfony 3). Both options are fine - it just depends on what's more important to you and how much you want to learn the new Symfony 4 stuff versus the actual content in this tutorial :).

    Cheers!

  • 2018-08-16 daro_G

    Hello,
    I'm trying to start that code on symfony 4, but I got this error:

    Cannot autowire service "App\Service\MarkdownTransformer": argument "$cache" of method "__construct()" references interface "Doctrine\Common\Cache\Cache" but no such service exists. You should maybe
    alias this interface to one of these existing services: "annotations.filesystem_cache", "annotations.cache", "doctrine_cache.providers.doctrine.orm.default_metadata_cache", "doctrine_cache.provider
    s.doctrine.orm.default_result_cache", "doctrine_cache.providers.doctrine.orm.default_query_cache".

    Does anybody know what to do?

  • 2018-02-14 weaverryan

    Hey ugo .p!

    Ah, thanks for the note! Yea, it looks like the latest version of the bundle doesn't support the version of Symfony we use in this tutorial (even though it would work just fine, actually). I'll add an issue on our end to add a note to help others :).

    Cheers!

  • 2018-02-14 ugo .p

    I figure out a way to fix the problem. I just installed the 1.2 version by running composer require stof/doctrine-extensions-bundle ~1.2
    It may help others ;)

  • 2018-02-14 ugo .p

    Hey guys,
    I dont understand why I can't install the stof bundle since I started with the course code. This is what I get from the terminal :
    - Installation request for symfony/symfony (locked at v3.1.4, required as 3.1.*) -> satisfiable by symfony/symfony[v3.1.4].
    Any clue?
    thx

  • 2017-07-24 Victor Bocharsky

    Hey maxii123 ,

    Take a look at my comment here: https://knpuniversity.com/s...
    The main idea is do not use Doctrine migrations for such a big and complex tasks like populating slugs in production where you have a lot of entities. Use one-time Symfony commands for it.

    Cheers!

  • 2017-07-24 maxii123

    Is there anything inherently wrong in this code. I ask because after running for 18 hours it has set 20 SLUGS..... Note I use the unit of work as setting slug to null wouldnt trigger persistance as it defaults to null.


    class Version20170723121131 extends AbstractMigration implements ContainerAwareInterface
    {

    use ContainerAwareTrait;

    /**
    * @param Schema $schema
    */
    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 person ADD slug VARCHAR(255) DEFAULT NULL');
    $this->addSql('CREATE UNIQUE INDEX UNIQ_34DCD176989D9B62 ON person (slug)');

    }

    public function postUp(Schema $schema)
    {
    $em = $this->container->get("doctrine.orm.entity_manager");

    $people = $em->getRepository(Person::class)->findAll();

    foreach ($people as $person) {
    $uow = $em->getUnitOfWork();
    $uow->propertyChanged($person, 'slug', NULL, NULL);
    $uow->scheduleForUpdate($person);
    $em->flush();
    }

    parent::postUp($schema); // TODO: Change the autogenerated stub

    }

    /**
    * @param Schema $schema
    */
    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('ALTER TABLE person DROP slug');
    }

    /**
    * Sets the container.
    *
    * @param ContainerInterface|null $container A ContainerInterface instance or null
    */
    public function setContainer(ContainerInterface $container = null)
    {
    $this->container = $container;
    }
    }
  • 2017-01-16 weaverryan

    Ah, strange indeed! We'll have to see if anyone has a similar issue as you in the future :).

  • 2017-01-15 ehymel

    This would not work for me until I set "nullable=true" on the $slug field in Genus entity. Since it wasn't working I was just going to allow it to be null and then manually fix things just to keep going. Once I did so then all worked without further manual intervention. Strange!