api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.45k stars 874 forks source link

translatable not working on item operations #1694

Closed remoteclient closed 6 years ago

remoteclient commented 6 years ago

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)),
        );
    }
}
ACC-Txomin commented 6 years ago

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?

remoteclient commented 6 years ago

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

ACC-Txomin commented 6 years ago

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

remoteclient commented 6 years ago

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.

remoteclient commented 6 years ago

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

ACC-Txomin commented 6 years ago

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

dunglas commented 6 years ago

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

remoteclient commented 6 years ago

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

soyuka commented 6 years ago

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

remoteclient commented 6 years ago

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.

do-web commented 6 years ago

@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?

remoteclient commented 6 years ago

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

do-web commented 6 years ago

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

SakhriHoussem commented 3 years ago

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()->useQueryCache(false);  //  <<======== need this for cache pb
            $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.

thank you very much , this worked for me

hamidzmi commented 3 years ago

Thanks, @remoteclient, furthermore I had a caching problem with the query in production environment, and I could solve it using the following statement: $queryBuilder = $queryBuilder->getQuery()->useQueryCache(false);

7system7 commented 3 years ago

Thanks @remoteclient. But sadly it is not working w/ nested properties, i.e. w/ serialization groups.

SakhriHoussem commented 3 years ago

Thanks, @remoteclient, furthermore I had a caching problem with the query in production environment, and I could solve it using the following statement: $queryBuilder = $queryBuilder->getQuery()->useQueryCache(false);

thanks bro yes i needed this because of cache problems

matveyI commented 2 years ago

Thanks @remoteclient. But sadly it is not working w/ nested properties, i.e. w/ serialization groups.

Indeed, translations don't work on nested properties.

However, I may have found a solution (maybe wrong, but still):

<?php

namespace App\Extension;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Query;
use Symfony\Component\HttpFoundation\RequestStack;
use Gedmo\Translatable\TranslatableListener;
use Gedmo\Translatable\Translatable;

class ResultItemExtension implements QueryResultItemExtensionInterface
{
    private $requestStack;

    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    public function getResult(QueryBuilder $queryBuilder)
    {        
        $query = $queryBuilder->getQuery();

        $query->setHint(
            Query::HINT_CUSTOM_OUTPUT_WALKER,
            'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
        );

        // locale
        $query->setHint(
            TranslatableListener::HINT_TRANSLATABLE_LOCALE,
            $this->requestStack->getCurrentRequest()->getLocale() // take locale from session or request etc.
        );
        // fallback
        $query->setHint(
            TranslatableListener::HINT_FALLBACK,
            1 // fallback to default values in case if record is not translated
        );

        return $query->getSingleResult();
    }

    public function supportsResult(string $resourceClass, string $operationName = null): bool
    {
        $reflection = new \ReflectionClass($resourceClass);
        return $reflection->implementsInterface(Translatable::class);
    }

    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, ?string $operationName = null, array $context = [])
    {
        return;
    }
}

I was implementing the getResult method from QueryResultItemExtensionInterface.

If you want to fetch a collection of entities in a single request, just set a negative priority in service.yaml (yeah, I know it's discouraged in the official documentation, but I'm the bad guy):

    App\Extension\ResultItemExtension:
        arguments:
            - '@request_stack'
        tags:
            - { name: api_platform.doctrine.orm.query_extension.item, priority: -9 }

Should work. The code was run on symfony 5.4 lts, api platform - v2.6.8

huynguyen93 commented 2 years ago

Thanks @remoteclient. But sadly it is not working w/ nested properties, i.e. w/ serialization groups.

I managed to make it work by setting fetch: 'EXTRA_LAZY' to the attribute.

kconde2 commented 2 years ago

Hello I'm facing same issue with pagination enabled with ApiPlatform 2.7.

<?php

namespace App\Doctrine\Extension;

use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Gedmo\Translatable\Query\TreeWalker\TranslationWalker;
use Gedmo\Translatable\Translatable;
use Gedmo\Translatable\TranslatableListener;
use ReflectionClass;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * class App\Doctrine\Extension
 */
class TranslatableResultExtension implements QueryCollectionExtensionInterface
{
    /**
     * @var RequestStack
     */
    private $requestStack;

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

    public function supports(string $resourceClass): bool
    {
        $reflection = new ReflectionClass($resourceClass);
        return $reflection->implementsInterface(Translatable::class);
    }

    /**
     * {@inheritdoc}
     */
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
    {
        if (!$this->supports($resourceClass)) {
            return;
        }

        $query = $queryBuilder->getQuery();
        $query->setHint(
            Query::HINT_CUSTOM_OUTPUT_WALKER,
            TranslationWalker::class
        );
        // locale
        $query->setHint(
            TranslatableListener::HINT_TRANSLATABLE_LOCALE,
            $this->requestStack->getCurrentRequest()->getLocale() // take locale from session or request etc.
        );
        // fallback
        $query->setHint(
            TranslatableListener::HINT_FALLBACK,
            1 // fallback to default values in case if record is not translated
        );
    }
}

Do you have any idea of why it is not working ? Thanks in advance

remoteclient commented 2 years ago

You haven't wrote what the issue is, but I guess that your collection doesn't get paginated. Have a look at the docs and see how to inject the extensions.

kconde2 commented 2 years ago

Hello I'm facing same issue with pagination enabled with ApiPlatform 2.7.

<?php

namespace App\Doctrine\Extension;

use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Gedmo\Translatable\Query\TreeWalker\TranslationWalker;
use Gedmo\Translatable\Translatable;
use Gedmo\Translatable\TranslatableListener;
use ReflectionClass;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * class App\Doctrine\Extension
 */
class TranslatableResultExtension implements QueryCollectionExtensionInterface
{
    /**
     * @var RequestStack
     */
    private $requestStack;

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

    public function supports(string $resourceClass): bool
    {
        $reflection = new ReflectionClass($resourceClass);
        return $reflection->implementsInterface(Translatable::class);
    }

    /**
     * {@inheritdoc}
     */
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
    {
        if (!$this->supports($resourceClass)) {
            return;
        }

        $query = $queryBuilder->getQuery();
        $query->setHint(
            Query::HINT_CUSTOM_OUTPUT_WALKER,
            TranslationWalker::class
        );
        // locale
        $query->setHint(
            TranslatableListener::HINT_TRANSLATABLE_LOCALE,
            $this->requestStack->getCurrentRequest()->getLocale() // take locale from session or request etc.
        );
        // fallback
        $query->setHint(
            TranslatableListener::HINT_FALLBACK,
            1 // fallback to default values in case if record is not translated
        );
    }
}

Do you have any idea of why it is not working ? Thanks in advance

Thanks @remoteclient

My issue is that $query->setHint is not applying to$queryBuilder I guess here https://github.com/api-platform/core/blob/main/src/Doctrine/Orm/State/CollectionProvider.php#L54

remoteclient commented 2 years ago

I have a custom Provider for that. You can make the support function more generic:

<?php
/**
 * Class TranslatableDataProvider.
 *
 * @author Martin Walther <martin@myweb.solutions>
 *
 * (c) MyWebSolutions
 */

declare(strict_types=1);

namespace MWS\NutritionCalculatorBundle\DataProvider;

use ApiPlatform\Core\Bridge\Doctrine\Orm\AbstractPaginator;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\PaginationExtension;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrineOrmPaginator;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Gedmo\Translatable\TranslatableListener;
use MWS\NutritionCalculatorBundle\Entity\DogBreed;
use MWS\NutritionCalculatorBundle\Entity\NutritionDatabase;
use MWS\NutritionCalculatorBundle\Entity\NutritionDatabaseMetadata;
use MWS\NutritionCalculatorBundle\Entity\NutritionDatabaseMetadataClassification;
use MWS\NutritionCalculatorBundle\Entity\NutritionDatabaseMetadataDimension;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Traversable;

class TranslatableCollectionDataProvider implements CollectionDataProviderInterface, RestrictedDataProviderInterface
{
    private $managerRegistry;
    private $requestStack;
    private $resourceMetadataFactory;
    private $enabled;
    private $clientEnabled;
    private $clientItemsPerPage;
    private $itemsPerPage;
    private $pageParameterName;
    private $enabledParameterName;
    private $itemsPerPageParameterName;
    private $maximumItemPerPage;
    private $partial;
    private $clientPartial;
    private $partialParameterName;
    private $collectionExtensions;

    /**
     * @param QueryCollectionExtensionInterface[] $collectionExtensions
     */
    public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack, ResourceMetadataFactoryInterface $resourceMetadataFactory, bool $enabled = true, bool $clientEnabled = false, bool $clientItemsPerPage = false, int $itemsPerPage = 30, string $pageParameterName = 'page', string $enabledParameterName = 'pagination', string $itemsPerPageParameterName = 'itemsPerPage', int $maximumItemPerPage = null, bool $partial = false, bool $clientPartial = false, string $partialParameterName = 'partial', array $collectionExtensions = [])
    {
        $this->managerRegistry = $managerRegistry;
        $this->requestStack = $requestStack;
        $this->resourceMetadataFactory = $resourceMetadataFactory;
        $this->enabled = $enabled;
        $this->clientEnabled = $clientEnabled;
        $this->clientItemsPerPage = $clientItemsPerPage;
        $this->itemsPerPage = $itemsPerPage;
        $this->pageParameterName = $pageParameterName;
        $this->enabledParameterName = $enabledParameterName;
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
        $this->maximumItemPerPage = $maximumItemPerPage;
        $this->partial = $partial;
        $this->clientPartial = $clientPartial;
        $this->partialParameterName = $partialParameterName;
        $this->collectionExtensions = $collectionExtensions;
    }

    public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
    {
        return DogBreed::class === $resourceClass
            || NutritionDatabase::class === $resourceClass
            || NutritionDatabaseMetadata::class === $resourceClass
            || NutritionDatabaseMetadataClassification::class === $resourceClass
            || NutritionDatabaseMetadataDimension::class === $resourceClass;
    }

    /**
     * Retrieves a collection.
     *
     * @return array|AbstractPaginator|Traversable
     *
     * @throws ResourceClassNotSupportedException
     * @throws ResourceClassNotFoundException
     */
    public function getCollection(string $resourceClass, string $operationName = null, array $context = [])
    {
        /** @var ObjectManager $manager */
        $manager = $this->managerRegistry->getManagerForClass($resourceClass);
        if (null === $manager) {
            throw new ResourceClassNotSupportedException();
        }

        $repository = $manager->getRepository($resourceClass);
        if (!method_exists($repository, 'createQueryBuilder')) {
            throw new RuntimeException('The repository class must have a "createQueryBuilder" method.');
        }

        $queryBuilder = $repository->createQueryBuilder('o');
        $queryNameGenerator = new QueryNameGenerator();
        foreach ($this->collectionExtensions as $extension) {
            /* @var PaginationExtension $extension */
            $extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);

            if ($extension instanceof PaginationExtension && $extension->supportsResult($resourceClass, $operationName, $context)) {
                /** @var QueryBuilder $queryBuilder */
                $query = $queryBuilder->getQuery()->useQueryCache(true)->enableResultCache();
                $query = $this->addTranslatableQueryHints($query);

                // Do the pagination again unless we can find a good solution here to reuse the code :|
                $doctrineOrmPaginator = new DoctrineOrmPaginator($query, $this->useFetchJoinCollection($queryBuilder));
                $doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder));
                $resourceMetadata = null === $resourceClass ? null : $this->resourceMetadataFactory->create($resourceClass);
                if ($this->isPartialPaginationEnabled($this->requestStack->getCurrentRequest(), $resourceMetadata, $operationName)) {
                    return new class($doctrineOrmPaginator) extends AbstractPaginator {
                    };
                }

                return new Paginator($doctrineOrmPaginator);
            }
        }

        /** @var QueryBuilder $queryBuilder */
        $query = $queryBuilder->getQuery()->useQueryCache(true)->enableResultCache();
        $query = $this->addTranslatableQueryHints($query);

        return $query->getResult();
    }

    /**
     * @param $query
     *
     * @return mixed
     */
    private function addTranslatableQueryHints(Query $query)
    {
        $query->setHint(
            Query::HINT_CUSTOM_OUTPUT_WALKER,
            'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
        );
        // locale
        $query->setHint(
            TranslatableListener::HINT_TRANSLATABLE_LOCALE,
            $this->requestStack->getCurrentRequest()->getLocale() // take locale from session or request etc.
        );
        // fallback
        $query->setHint(
            TranslatableListener::HINT_FALLBACK,
            0 // fallback to default values in case if record is not translated
        );
        $query->setHint(TranslatableListener::HINT_INNER_JOIN, true);
//        $query->setHydrationMode(TranslationWalker::HYDRATE_OBJECT_TRANSLATION);
//        $query->setHint(Query::HINT_REFRESH, true);

        return $query;
    }

    /**
     * Determines whether the Paginator should fetch join collections, if the root entity uses composite identifiers it should not.
     *
     * @see https://github.com/doctrine/doctrine2/issues/2910
     */
    private function useFetchJoinCollection(QueryBuilder $queryBuilder): bool
    {
        return !QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry);
    }

    /**
     * Determines whether output walkers should be used.
     */
    private function useOutputWalkers(QueryBuilder $queryBuilder): bool
    {
        /*
         * "Cannot count query that uses a HAVING clause. Use the output walkers for pagination"
         *
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L50
         */
        if (QueryChecker::hasHavingClause($queryBuilder)) {
            return true;
        }

        /*
         * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator."
         *
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L87
         */
        if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) {
            return true;
        }

        /*
         * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers."
         *
         * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L149
         */
        if (
            QueryChecker::hasMaxResults($queryBuilder) &&
            QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $this->managerRegistry)
        ) {
            return true;
        }

        /*
         * When using composite identifiers pagination will need Output walkers
         */
        if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) {
            return true;
        }

        // Disable output walkers by default (performance)
        return false;
    }

    private function isPartialPaginationEnabled(Request $request = null, ResourceMetadata $resourceMetadata = null, string $operationName = null): bool
    {
        $enabled = $this->partial;
        $clientEnabled = $this->clientPartial;

        if ($resourceMetadata) {
            $enabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_partial', $enabled, true);

            if ($request) {
                $clientEnabled = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_partial', $clientEnabled, true);
            }
        }

        if ($clientEnabled && $request) {
            $enabled = filter_var($this->getPaginationParameter($request, $this->partialParameterName, $enabled), FILTER_VALIDATE_BOOLEAN);
        }

        return $enabled;
    }

    private function getPaginationParameter(Request $request, string $parameterName, $default = null)
    {
        if (null !== $paginationAttribute = $request->attributes->get('_api_pagination')) {
            return \array_key_exists($parameterName, $paginationAttribute) ? $paginationAttribute[$parameterName] : $default;
        }

        return $request->query->get($parameterName, $default);
    }
}
kconde2 commented 2 years ago

@remoteclient How did you declare this provider in services.yaml ?

remoteclient commented 2 years ago

I use xml for configuration:

    <services>
        <service id="mws_nc.data_provider.translatable_collection_data_provider"
                 class="MWS\NutritionCalculatorBundle\DataProvider\TranslatableCollectionDataProvider"
        >
            <tag name="api_platform.collection_data_provider" priority="2"/>
            <argument type="service" id="doctrine"/>
            <argument type="service" id="request_stack"/>
            <argument type="service" id="api_platform.metadata.resource.metadata_factory" />
            <argument>%api_platform.collection.pagination.enabled%</argument>
            <argument>%api_platform.collection.pagination.client_enabled%</argument>
            <argument>%api_platform.collection.pagination.client_items_per_page%</argument>
            <argument>%api_platform.collection.pagination.items_per_page%</argument>
            <argument>%api_platform.collection.pagination.page_parameter_name%</argument>
            <argument>%api_platform.collection.pagination.enabled_parameter_name%</argument>
            <argument>%api_platform.collection.pagination.items_per_page_parameter_name%</argument>
            <argument>%api_platform.collection.pagination.maximum_items_per_page%</argument>
            <argument>%api_platform.collection.pagination.partial%</argument>
            <argument>%api_platform.collection.pagination.client_partial%</argument>
            <argument>%api_platform.collection.pagination.partial_parameter_name%</argument>
            <argument type="collection">
                <argument type="service" id="api_platform.doctrine.orm.query_extension.eager_loading"/>
                <argument type="service" id="api_platform.doctrine.orm.query_extension.filter"/>
                <argument type="service" id="api_platform.doctrine.orm.query_extension.filter_eager_loading"/>
                <argument type="service" id="api_platform.doctrine.orm.query_extension.order"/>
                <argument type="service" id="api_platform.doctrine.orm.query_extension.pagination"/>
            </argument>
        </service>
kconde2 commented 2 years ago

I use xml for configuration:

    <services>
        <service id="mws_nc.data_provider.translatable_collection_data_provider"
                 class="MWS\NutritionCalculatorBundle\DataProvider\TranslatableCollectionDataProvider"
        >
            <tag name="api_platform.collection_data_provider" priority="2"/>
            <argument type="service" id="doctrine"/>
            <argument type="service" id="request_stack"/>
            <argument type="service" id="api_platform.metadata.resource.metadata_factory" />
            <argument>%api_platform.collection.pagination.enabled%</argument>
            <argument>%api_platform.collection.pagination.client_enabled%</argument>
            <argument>%api_platform.collection.pagination.client_items_per_page%</argument>
            <argument>%api_platform.collection.pagination.items_per_page%</argument>
            <argument>%api_platform.collection.pagination.page_parameter_name%</argument>
            <argument>%api_platform.collection.pagination.enabled_parameter_name%</argument>
            <argument>%api_platform.collection.pagination.items_per_page_parameter_name%</argument>
            <argument>%api_platform.collection.pagination.maximum_items_per_page%</argument>
            <argument>%api_platform.collection.pagination.partial%</argument>
            <argument>%api_platform.collection.pagination.client_partial%</argument>
            <argument>%api_platform.collection.pagination.partial_parameter_name%</argument>
            <argument type="collection">
                <argument type="service" id="api_platform.doctrine.orm.query_extension.eager_loading"/>
                <argument type="service" id="api_platform.doctrine.orm.query_extension.filter"/>
                <argument type="service" id="api_platform.doctrine.orm.query_extension.filter_eager_loading"/>
                <argument type="service" id="api_platform.doctrine.orm.query_extension.order"/>
                <argument type="service" id="api_platform.doctrine.orm.query_extension.pagination"/>
            </argument>
        </service>

It's working :+1: Thanks a lot!

enesdayanc commented 1 year ago

you can check my ticket for cleaner implementation https://github.com/api-platform/core/issues/5412