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:

composer require stof/doctrine-extensions-bundle

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!

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