Gedmo Translations in Symfony 4

Today I’ve had to install Gedmo Translations into a Symfony 4 app and I had some trouble, so after fixing them problems I thought of writing them down in case someone else can benefit from it 🙂

Let’s start fromt he begginig.

Create a Symfony 4 project

Please use docker! 🙂 More info here in the Docker for Symfony 4 post.

Here’s what my docker.compose.yml looks like

#docker.compose.yml
version: "3.1"

volumes:
    db-data:

services:
    mysql:
      image: mysql:5.6
      container_name: ${PROJECT_NAME}-mysql
      working_dir: /application
      volumes:
        - db-data:/application
      environment:
        - MYSQL_ROOT_PASSWORD=docker_root
        - MYSQL_DATABASE=gedmoapp_db
        - MYSQL_USER=gedmoapp_user
        - MYSQL_PASSWORD=gedmoapp_pw
      ports:
        - "8306:3306"

    webserver:
      image: nginx:alpine
      container_name: ${PROJECT_NAME}-webserver
      working_dir: /application
      volumes:
        - .:/application
        - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
      ports:
        - "8000:80"

    php-fpm:
      build: docker/php-fpm
      container_name: ${PROJECT_NAME}-php-fpm
      working_dir: /application
      volumes:
        - .:/application
        - ./docker/php-fpm/php-ini-overrides.ini:/etc/php/7.2/fpm/conf.d/99-overrides.ini

Installation of Gedmo’s bundle

composer require stof/doctrine-extensions-bundle

Configuration

Now we need to update our configuration file, so in the doctrine.yaml, under the doctrine key, we should go from this

#doctrine.yaml
doctrine:
#...
    orm:
        auto_generate_proxy_classes: '%kernel.debug%'
        naming_strategy: doctrine.orm.naming_strategy.underscore
        auto_mapping: true
        naming_strategy: doctrine.orm.naming_strategy.underscore
        auto_mapping: true
        mappings:
            App:
                is_bundle: false
                type: annotation
                dir: '%kernel.project_dir%/src/Entity'
                prefix: 'App\Entity'
                alias: App

To this

#doctrine.yaml
doctrine:
#...
	orm:
        auto_generate_proxy_classes: '%kernel.debug%'
#        naming_strategy: doctrine.orm.naming_strategy.underscore
#        auto_mapping: true
        entity_managers:
            default:
#                connection: default
                naming_strategy: doctrine.orm.naming_strategy.underscore
                auto_mapping: true
                mappings:
                    App:
                        is_bundle: false
                        type: annotation
                        dir: '%kernel.project_dir%/src/Entity'
                        prefix: 'App\Entity'
                        alias: App
                    gedmo_translatable:
                        type: annotation
                        prefix: Gedmo\Translatable\Entity
                        dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity"
                        alias: GedmoTranslatable # (optional) it will default to the name set for the mapping
                        is_bundle: false
                    gedmo_translator:
                        type: annotation
                        prefix: Gedmo\Translator\Entity
                        dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translator/Entity"
                        alias: GedmoTranslator # (optional) it will default to the name set for the mapping
                        is_bundle: false
#                    gedmo_loggable:
#                        type: annotation
#                        prefix: Gedmo\Loggable\Entity
#                        dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity"
#                        alias: GedmoLoggable # (optional) it will default to the name set for the mappingmapping
#                        is_bundle: false
                    gedmo_tree:
                        type: annotation
                        prefix: Gedmo\Tree\Entity
                        dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Tree/Entity"
                        alias: GedmoTree # (optional) it will default to the name set for the mapping
                        is_bundle: false

Also, in our stof_doctrine_extensions.yaml, let’s add the following configuration for the doctrine extensions.

#stof_doctrine_extensions.yaml
stof_doctrine_extensions:
    default_locale: en_US
    orm:
        default:
            tree: true
            translatable: true
            sluggable: true

Now, if we try to update our schema

php bin/console doc:sch:update --dump-sql
The following SQL statements will be executed:

     CREATE TABLE ext_translations (id INT AUTO_INCREMENT NOT NULL, locale VARCHAR(8) NOT NULL, object_class VARCHAR(255) NOT NULL, field VARCHAR(32) NOT NULL, foreign_key VARCHAR(64) NOT NULL, content LONGTEXT DEFAULT NULL, INDEX translations_lookup_idx (locale, object_class, foreign_key), UNIQUE INDEX lookup_unique_idx (locale, object_class, field, foreign_key), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB ROW_FORMAT = DYNAMIC;

Looks about right, so let’s hit it!

php bin/console doc:sch:update --force

And we get the following error

Updating database schema...


In AbstractMySQLDriver.php line 126:
                                                                                                                                
  An exception occurred while executing 'ALTER TABLE ext_translations CHANGE object_class object_class VARCHAR(255) NOT NULL':  
                                                                                                                                
  SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes               
                                                                                                                                

In PDOConnection.php line 109:
                                                                                                                   
  SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes  
                                                                                                                   

In PDOConnection.php line 107:
                                                                                                                   
  SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes

Now, after looking for this issue with Symfony and Gedmo Translations in the internet, I found this issue about Doctrine and this other one, and the fix here.

As davidbehler and javiereguiluz explain in the posts,

the cause of this is that Symfony advises you to use utf8mb4_general_ci/utf8mb4 as collation/charset for your database. utf8mb4 takes 4 bytes per char, meaning a 255 char field needs 1020 bytes for an index.

And so it seems that MySql 5.6 has a max key length of 767 bytes, so that leaves us with three options:

1. Decrease the field length to 191. However, we would have to override Gedmo’s translations bundle, as it uses 255 char fields everywhere…

2. We could change the charset encoding to ut8 like so (thanks to Kuba Florczuk for the configuration):

#doctrine.yaml
doctrine:
    dbal:
        # configure these for your database server
        driver: 'pdo_mysql'
        server_version: '5.6'
        charset: utf8
        default_table_options:
            charset: utf8
            collate: utf8_unicode_ci

        url: '%env(resolve:DATABASE_URL)%'
#...

3. We could upgrade to mysql 5.7 like so:

#docker-compose.yml
services:
    mysql:
      image: mysql:5.7
#...
#doctrine.yaml
doctrine:
    dbal:
        # configure these for your database server
        driver: 'pdo_mysql'
        server_version: '5.7'
        charset: utf8mb4
        default_table_options:
            charset: utf8mb4
            collate: utf8mb4_unicode_ci

        url: '%env(resolve:DATABASE_URL)%'
#...

All of them solutions work gracefully. Hope this helps, happy coding! 🙂

Gedmo’s Translations Documentation

StofDoctrineExensionBundle in Symfony’s official website.
Doctrine’s extensions GitHub doc.
Translatable’s GitHub doc.

Gedmo Tree in Symfony3

So today at work I’ve had to rethink the way we work with products in an app, and it turns out the best way to face the problem is having a tree based architecture, as products can have sub-products and so on and so forth… so instead of reinventing the wheel, why not use a well established library that uses trees right? Gedmo please!

So it’s actually really easy to install Gedmo extensions to your project.

1) First of all composer install it

php -d memory_limit=-1 /usr/local/bin/composer require stof/doctrine-extensions-bundle

(yes I don’t like playing with my php.ini so for this command I rather pass the memory_limit parameter, also remember to put your composer path correctly, as it won’t read your alias if you pass parameters to php)

2) After that you need to include it to you Kernel

$bundles = [
			...
            // Gedmo
            new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
            ...
        ];

3) And the final step is adding the extra configuration in your config.yml

Under the doctrine.orm configuration:

# Doctrine Configuration
doctrine:
    ...

    orm:
        auto_generate_proxy_classes: "%kernel.debug%"
#        naming_strategy: doctrine.orm.naming_strategy.underscore
#        auto_mapping: true

        default_entity_manager: default
        entity_managers:
            default:
              connection: default
              naming_strategy: doctrine.orm.naming_strategy.underscore
              auto_mapping: true
              dql:
                  numeric_functions:
                      ACOS: DoctrineExtensions\Query\Mysql\Acos
                      COS: DoctrineExtensions\Query\Mysql\Cos
                      RADIANS: DoctrineExtensions\Query\Mysql\Radians
                      SIN: DoctrineExtensions\Query\Mysql\Sin
              mappings:
                  gedmo_translatable:
                      type: annotation
                      prefix: Gedmo\Translatable\Entity
                      dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity"
                      alias: GedmoTranslatable # (optional) it will default to the name set for the mapping
                      is_bundle: false
                  gedmo_translator:
                      type: annotation
                      prefix: Gedmo\Translator\Entity
                      dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translator/Entity"
                      alias: GedmoTranslator # (optional) it will default to the name set for the mapping
                      is_bundle: false
                  gedmo_loggable:
                      type: annotation
                      prefix: Gedmo\Loggable\Entity
                      dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity"
                      alias: GedmoLoggable # (optional) it will default to the name set for the mappingmapping
                      is_bundle: false
                  gedmo_tree:
                      type: annotation
                      prefix: Gedmo\Tree\Entity
                      dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Tree/Entity"
                      alias: GedmoTree # (optional) it will default to the name set for the mapping
                      is_bundle: false

Yes, it’s important to remove the auto_mapping as it won’t recognize the key under doctrine.orm

And under the stof_doctrine_extensions configuration:

# Stof Configuration
stof_doctrine_extensions:
    default_locale: "%locale%"
    translation_fallback: true
    persist_default_translation: true

    # Only used if you activated the Uploadable extension
    uploadable:
        #stof_doctrine_extensions.uploadable.validate_writable_directory
        validate_writable_directory: true

        # Default file path: This is one of the three ways you can configure the path for the Uploadable extension
        default_file_path:       "%kernel.root_dir%/../web/uploads"

        # Mime type guesser class: Optional. By default, we provide an adapter for the one present in the HttpFoundation component of Symfony
        mime_type_guesser_class: Stof\DoctrineExtensionsBundle\Uploadable\MimeTypeGuesserAdapter

        # Default file info class implementing FileInfoInterface: Optional. By default we provide a class which is prepared to receive an UploadedFile instance.
        default_file_info_class: Stof\DoctrineExtensionsBundle\Uploadable\UploadedFileInfo
    orm:
        default:
            translatable:   true
            blameable:      false
            timestampable:  true
            tree:           true
            uploadable:     false
            sluggable:      true

That’s it! You’re set, super easy right?

Now let’s go to the entity, in my case it’s the Product. Don’t forget to import the Gedmo annotation and then add the annotation to the class, like so:

use Gedmo\Mapping\Annotation as Gedmo;

/**
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
 * ...
 * @Gedmo\Tree(type="nested")
 */
class Product
{
	/**
     * @Gedmo\TreeLeft
     * @ORM\Column(name="lft", type="integer")
     */
    private $lft;

    /**
     * @Gedmo\TreeLevel
     * @ORM\Column(name="lvl", type="integer")
     */
    private $lvl;

    /**
     * @Gedmo\TreeRight
     * @ORM\Column(name="rgt", type="integer")
     */
    private $rgt;

    /**
     * @Gedmo\TreeRoot
     * @ORM\ManyToOne(targetEntity="Product")
     * @ORM\JoinColumn(name="tree_root", referencedColumnName="id", onDelete="CASCADE")
     */
    private $root;

    /**
     * @Gedmo\TreeParent
     * @ORM\ManyToOne(targetEntity="Product", inversedBy="children")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
     */
    private $parent;

    /**
     * @ORM\OneToMany(targetEntity="Product", mappedBy="parent")
     * @ORM\OrderBy({"lft" = "ASC"})
     */
    private $children;

    ...
}

Running the command

bin/console doc:gen:entities AppBundle:Product

will generate all the getters and setters, pretty handy…

And now the repository:

use Gedmo\Tree\Entity\Repository\NestedTreeRepository;

/**
 * ProductRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class ProductRepository extends NestedTreeRepository
{
...
}

The NestedTreeRepository has very useful functions, play with them before making your custom ones, cause they’ve already thought with pretty much everything, for instance the function childrenHierarchy() gives you the nested array, with decoration output if you pass parameters as options. You can find more info in the RepositoryInterface and RepositoryUtilsInterface from Gedmo\Tree namespace.

PS: Don’t forget to run your schema update command

bin/console doc:sch:update --force

Now you focus on what really matters, you project’s logic.

Happy coding! 🙂