Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

DoctrineExtensions: Sluggable

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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

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.3"

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

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

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

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

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

12
Login or Register to join the conversation

Wouldn't a better practice be to append genus-id rather than adding a new column to the database?

Reply

Hey @Daniel

What you mean by "Append genus-id"? It's a common practice to store the slug in a field so you can ensure two things
1) All slugs are unique
2) A slug won't change if you edit such record

Cheers!

1 Reply
Dariusz Avatar
Dariusz Avatar Dariusz | posted 5 years ago

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?

Reply

Hey Dariusz!

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!

Reply
Richard Avatar
Richard Avatar Richard | posted 5 years ago | edited

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;
    }
}
Reply

Hey Richard ,

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!

Reply
Ernest H. Avatar
Ernest H. Avatar Ernest H. | posted 5 years ago

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!

Reply

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

Reply
Default user avatar

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

Reply
Default user avatar

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 ;)

2 Reply

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!

Reply
Cat in space

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

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice