Core: translatable not working on item operations

Created on 8 Feb 2018  路  13Comments  路  Source: api-platform/core

I am using StofDoctrineExtensionsBundle to translate entities. While it is working perfectly on collection operations, it fails on item operations and the returned fields are always in default locale.
I use an event subscriber to set the locale:

<?php

namespace MWS\UserBundle\EventSubscriber;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class LocaleSubscriber implements EventSubscriberInterface
{
    private $defaultLocale;

    public function __construct($defaultLocale = 'en_US')
    {
        $this->defaultLocale = $defaultLocale;
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();

        // try to see if the locale has been set as a accept-language routing parameter
        if ($locale = $request->headers->get('accept-language')) {
            $request->getSession()->set('_locale', $locale);
            $request->setLocale($locale);
        } else {
            // if no explicit locale has been set on this request, use one from the session
            $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
        }
    }

    public static function getSubscribedEvents()
    {
        return array(
            // must be registered after the default Locale listener
            KernelEvents::REQUEST => array(array('onKernelRequest', 15)),
        );
    }
}

Most helpful comment

Today I can come up with a solution. The answer lies not in a custom action but in a custom extension described here. You have to set hints for the query described in the documentation of gedmo translatable.
This is my implementation:

<?php
namespace MWS\NutritionCalculatorBundle\Doctrine;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Gedmo\Translatable\Query\TreeWalker\TranslationWalker;
use Gedmo\Translatable\TranslatableListener;
use MWS\NutritionCalculatorBundle\Entity\DogBreed;
use Symfony\Component\HttpFoundation\RequestStack;

final class DogBreedExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    /**
     * @var RequestStack
     */
    private $requestStack;

    /**
     * DogBreedExtension constructor.
     * @param RequestStack $requestStack
     */
    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        $this->addHints($queryBuilder, $resourceClass);
    }

    /**
     * @param QueryBuilder $queryBuilder
     * @param QueryNameGeneratorInterface $queryNameGenerator
     * @param string $resourceClass
     * @param array $identifiers
     * @param string|null $operationName
     * @param array $context
     */
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
    {
        $this->addHints($queryBuilder, $resourceClass);
    }

    /**
     *
     * @param QueryBuilder $queryBuilder
     * @param string       $resourceClass
     */
    private function addHints(QueryBuilder $queryBuilder, string $resourceClass)
    {
        if (DogBreed::class === $resourceClass) {
            $queryBuilder = $queryBuilder->getQuery();
            $queryBuilder->setHint(
                Query::HINT_CUSTOM_OUTPUT_WALKER,
                'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
            );
            // locale
            $queryBuilder->setHint(
                TranslatableListener::HINT_TRANSLATABLE_LOCALE,
                $this->requestStack->getCurrentRequest()->getLocale() // take locale from session or request etc.
            );
            // fallback
            $queryBuilder->setHint(
                TranslatableListener::HINT_FALLBACK,
                1 // fallback to default values in case if record is not translated
            );
            $queryBuilder->getResult();
        }
    }
}

Adding hints to the collection request is not necessary, but it gives you one database query for the list view instead of multiple queries depending on your pagination size.

All 13 comments

Same issue here.
Collection operations, GET and POST, works fine.
Item operation PUT works fine but GET operation gives default language content instead localized content.

@remoteclient, have you been able to solve it?

@ACC-Txomin The best way I think would be to hook into the query. But I don't know how to do it right now. In the meantime one could set up a custom action to retrieve the translation and expose it.

@remoteclient Thanks for your fast reply.
I think I'm going to do a custom action.
I hope eventually this bug get solved.

Today I can come up with a solution. The answer lies not in a custom action but in a custom extension described here. You have to set hints for the query described in the documentation of gedmo translatable.
This is my implementation:

<?php
namespace MWS\NutritionCalculatorBundle\Doctrine;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Gedmo\Translatable\Query\TreeWalker\TranslationWalker;
use Gedmo\Translatable\TranslatableListener;
use MWS\NutritionCalculatorBundle\Entity\DogBreed;
use Symfony\Component\HttpFoundation\RequestStack;

final class DogBreedExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    /**
     * @var RequestStack
     */
    private $requestStack;

    /**
     * DogBreedExtension constructor.
     * @param RequestStack $requestStack
     */
    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        $this->addHints($queryBuilder, $resourceClass);
    }

    /**
     * @param QueryBuilder $queryBuilder
     * @param QueryNameGeneratorInterface $queryNameGenerator
     * @param string $resourceClass
     * @param array $identifiers
     * @param string|null $operationName
     * @param array $context
     */
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
    {
        $this->addHints($queryBuilder, $resourceClass);
    }

    /**
     *
     * @param QueryBuilder $queryBuilder
     * @param string       $resourceClass
     */
    private function addHints(QueryBuilder $queryBuilder, string $resourceClass)
    {
        if (DogBreed::class === $resourceClass) {
            $queryBuilder = $queryBuilder->getQuery();
            $queryBuilder->setHint(
                Query::HINT_CUSTOM_OUTPUT_WALKER,
                'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
            );
            // locale
            $queryBuilder->setHint(
                TranslatableListener::HINT_TRANSLATABLE_LOCALE,
                $this->requestStack->getCurrentRequest()->getLocale() // take locale from session or request etc.
            );
            // fallback
            $queryBuilder->setHint(
                TranslatableListener::HINT_FALLBACK,
                1 // fallback to default values in case if record is not translated
            );
            $queryBuilder->getResult();
        }
    }
}

Adding hints to the collection request is not necessary, but it gives you one database query for the list view instead of multiple queries depending on your pagination size.

@ACC-Txomin please leave a message if this works for you too. Then I can close this issue.

@remoteclient works like a charm!! Thank you very much.

It is worth an entry in the StofDoctrineExtensionsBundle's or API Platform docs.

@dunglas Thank you. I wanted to ask you tomorrow on slack if I should make a doc entry on apip. Under which section should an update been posted? Directly unter extensions, or should there be something new?

I also changed the listener a bit and removed the session as there is none in stateless connections. should this also be posted? Then a new section "StofDoctrineExtensionsBundle Integration would be good.

Would be good enough if we had an entry in apiplatform docs. I'd put it under https://api-platform.com/docs/core/extensions

There needs some more to be done. As I realized now, filtering on translated entities works not like it is expected. Example: My default language is en_US. The second language I use is de_DE. Now I set the language to de_DE in the header to retrieve only the german translations. What the filter is doing right now is that it filters the english collection and then it gives the german translations for it back.

@remoteclient i also tried to add StofDoctrineExtensionsBundle. But how do you add translated records?

First i post this:

{
    "id": 1,
    "name": "Test en_US",
}

Then i set the request locale to de_DE and POST this, but it does not work.

{
    "id": 1,
    "name": "Test de_DE",
}

Have you an example how this should work?

@do-web StofDoctrineExtensionsBundle uses DoctrineExtensions. The way described in the documentation does it on a step by step basis.
First POST an entity in default language. For translation PUT to that entity the new translated entity and set translatable locale to the one the fits.

Thanks, i have no my own translatable extension for api-platform. :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

gustawdaniel picture gustawdaniel  路  3Comments

theshaunwalker picture theshaunwalker  路  3Comments

dematerializer picture dematerializer  路  3Comments

desmax picture desmax  路  3Comments

mahmoodbazdar picture mahmoodbazdar  路  3Comments