Supply validation group context with Symfony / API Platform - symfony

As I said in the title I try to supply the validation context of Sf / Api platform.
More precisely I would like to have different validation groups depending on an entity value.
If i'm a User with ROLE_PRO : then i want validate:pro and
default as validation groups.
If i'm a User with ROLE_USER : then i want default as validation
group.
I tried to create an event based on the following api-platform event but I can't find a way to supply the ExecutionContextInterface with my validation groups
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['addGroups', EventPriorities::PRE_VALIDATE],
];
}

As you can see in api-platform documentation (https://api-platform.com/docs/core/serialization/#changing-the-serialization-context-dynamically) you can manipulate validation groups dynamically with a service.
First of all, in your api-platform configuration, you have to define default validation group:
App\Class\MyClass:
properties:
id:
identifier: true
attributes:
input: false
normalization_context:
groups: ['default']
You need to define a new service which implements SerializerContextBuilderInterface
class ContextBuilder implements SerializerContextBuilderInterface
{
private SerializerContextBuilderInterface $decorated;
private AuthorizationCheckerInterface $authorizationChecker;
public function __construct(SerializerContextBuilderInterface $decorated, AuthorizationCheckerInterface $authorizationChecker)
{
$this->decorated = $decorated;
$this->authorizationChecker = $authorizationChecker;
}
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
if (isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_PRO') && true === $normalization) {
$context['groups'][] = 'validate:pro';
}
return $context;
}
}
Also, you need to configure your new service with a decorator
App\Builder\ContextBuilder:
decorates: 'api_platform.serializer.context_builder'
arguments: [ '#App\Builder\ContextBuilder.inner' ]
What it's happening here is:
You're overriding the ContextBuilder. First of all you create the context from request and from configuration (first line of createFromRequest method) and after this, you modify the context depeding of which user is logged.
Thanks!

Related

Add dynamic filters or pagination in ContextBuilder using API Platform

I am trying to force filters or pagination dynamically using a ContextBuilder.
For example, I want to force pagination for the group public:read:
namespace App\Serializer;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
final class FooContextBuilder implements SerializerContextBuilderInterface
{
private $decorated;
public function __construct(SerializerContextBuilderInterface $decorated)
{
$this->decorated = $decorated;
}
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
if (($resourceClass === Foo::class
$context['operation_type'] ?? null) === 'collection' &&
true === $normalization
) {
if ((isset($context['groups']) &&
in_array('public:read', $context['groups'])
) {
$context['filters']['pagination'] = true;
}
}
return $context;
}
}
services.yml:
services:
...
App\Serializer\RouteContextBuilder:
decorates: 'api_platform.serializer.context_builder'
arguments: [ '#App\Serializer\RouteContextBuilder.inner' ]
autoconfigure: false
Unfortunately, it seems that $context['filters'] is built as a later stage as it is not available in the ContextBuilder yet. $context['filters'] is available later e.g. in a DataProvider.
I tried to change the decoration priority in services.yml without success:
services:
App\Serializer\RouteContextBuilder:
...
decoration_priority: -1
How can I add dynamic filters or pagination through the context? Is there another interface that can be decorated which is called a later stage of the normalization process and before the filters are applied?
The serialization process is executed after data retrieval this can't work. Use a data Provider.

Doctrine filter not working on itemOperations (Api-Platform)

API Platform version(s) affected: 2.6.8
Description
In a project which uses PostgreSQL and API-Platform, I need to filter all records by a locale string. A doctrine filter is my preferred choice to do so.
This is the filter:
class LocaleFilter extends SQLFilter
{
public const LOCALE_FILTER_NAME = 'locale_filter';
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if (!$targetEntity->reflClass->implementsInterface(LocalizedEntityInterface::class)) {
return '';
}
return $targetTableAlias . '.locale = ' . $this->getParameter('locale');
}
}
The parameter locale will be set on each onKernelRequest event, the locale is the value of the header X-Locale:
public function onKernelRequest(RequestEvent $event): void
{
$locale = $event->getRequest()->headers->get('X-Locale');
$this->setFilterLocale($locale);
}
private function setFilterLocale(string $locale): void
{
if (!$this->entityManager->hasFilters()) {
return;
}
$localeFilter = $this->entityManager->getFilters()->getFilter(LocaleFilter::LOCALE_FILTER_NAME);
$localeFilter->setParameter('locale', $locale);
}
Now, when I send a request to a collectionOperations endpoint, such as http://example.com/products with the X-Locale header value de_DE, the filter is working and I get a response which contains only the according data in de_DE. When I send a request with locale fr_FR, I get a response with data in fr_FR.
But, when I send a request with the same X-Locale header to a itemOperations endpoint like http://example.com/products/<a-existing-id> I'm getting the error message The parameter "locale" is not set which comes from doctrine.
After investigating that issue, I can say that it works when I override the default ItemDataProvider from API-platform:
<?php
namespace App\DataProvider;
[...]
class ItemDataProvider implements ItemDataProviderInterface
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly RequestStack $requestStack,
) {
}
public function getItem(string $resourceClass, $id, ?string $operationName = null, array $context = []): object
{
$locale = $this->requestStack->getMainRequest()->headers->get('X-Locale');
if ($this->entityManager->hasFilters()) {
$localeFilter = $this->entityManager->getFilters()->getFilter(LocaleFilter::LOCALE_FILTER_NAME);
$localeFilter->setParameter('locale', $locale);
}
$query = $this->entityManager->getRepository($resourceClass)
->createQueryBuilder('x')
->where('x.publicId = :pubid')
->setParameter('pubid', $id)
->getQuery();
return $query->getOneOrNullResult();
}
}
But is still required to set the filter value again in "my own" ItemDataProvider. If I delete the first 7 lines of the method getItem of the ItemDataProvider, I get the same error from above.
That doesn't make sense like that, does it? It seems like Api-Platform overrides the Doctrine filters in the default ItemDataProvider and make them useless. Howewer, I didn't found the reason for that issue.
Overriding the ItemDataProvider is a working workaround, but I don't think it's a good one, since the cause is more likely a bug and that way some features of Api-Platform are no longer present in the whole project.

Symfony 4: I decorated UrlGeneratorInterface, but it's not used, it uses CompiledUrlGenerator instead

I decorated UrlGeneratorInterface
app.decorator.url_generator:
class: App\CoreBundle\Routing\Extension\UrlGenerator
decorates: Symfony\Component\Routing\Generator\UrlGeneratorInterface
arguments: ['#app.decorator.url_generator.inner']
but it's not used in cases where some bundle in example executes $this->generator->generate(), and I tracked what Symfony does through XDebug and CompiledUrlGenerator is used instead. I can see where this happens, namely in Symfony\Component\Routing\Router in getGenerator it specifically checks for CompiledUrlGenerator::class. But I don't want to override vanilla Symfony code. How am I supposed to override/decorate/extend which class in order for mine to be chosen always, as I have special parameters I need to add to the path. Thank you in advance!
I found it.
app.decorator.router:
class: App\CoreBundle\Routing\Extension\Router
decorates: 'router.default'
arguments: ['#app.decorator.router.inner']
Decorating this actually makes all packages use your Router. And as the UrlGenerator it has the generate function which can be extended.
EDIT: On request I provide the router class as well:
class Router implements RouterInterface {
protected $innerRouter;
public function __construct(RouterInterface $innerRouter) {
$this->innerRouter = $innerRouter;
}
public function setContext(RequestContext $context)
{
$this->innerRouter->setContext($context);
}
public function getContext()
{
return $this->innerRouter->getContext();
}
public function getRouteCollection()
{
return $this->innerRouter->getRouteCollection();
}
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
//add here to $parameters...
return $this->innerRouter->generate($name, $parameters, $referenceType);
}
public function match($pathinfo)
{
$parameters = $this->innerRouter->match($pathinfo);
//add here to $parameters...
return $parameters;
}
}

How to globally define configuration of Serializer used in API Platform

In order to user the #MaxDepth annotation in entities, the enable_max_depth property has to be set explicitly in serializer context (e.g. in config of the #ApiPlatform annotation), so on entity level, so for each entity
Is there a way to define this property enable_max_depth=true for all entities of the project ? Something we could find in api-platform.yaml and which will look like that :
api-platform:
serializer:
enable_max_depth: true
There is no such global option for now (it can be worth adding it, PR welcome).
However, you can register a SerializerContextBuilder to add this context entry automatically for all resources:
<?php
namespace App\Serializer;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
final class MaxDepthContextBuilder implements SerializerContextBuilderInterface
{
private $decorated;
public function __construct(SerializerContextBuilderInterface $decorated)
{
$this->decorated = $decorated;
}
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
$context['enable_max_depth'] = true;
return $context;
}
}
Then register this new class as a service decorator:
# api/config/services.yaml
services:
# ...
'App\Serializer\MaxDepthContextBuilder':
decorates: 'api_platform.serializer.context_builder'
autoconfigure: false
autowire: true

Symfony Factory class

I would like to use a ResultFactory class as a service in my Symfony 2 application:
My Result factory class will be responsible to create a BaseResult instance.
Depending on the type passed to the get factory method, the ResultFactory will create the right ResultObject.
Here's what could be the code:
class ResultFactory
{
protected $translator;
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
public function get($type, $param)
{
$instance = null;
switch ($type) {
case 'Type1':
$instance = new Type1Result($param);
break;
case 'Type2':
$instance = new Type2Result($param);
break;
}
return $instance;
}
}
My question is:
I would like to use a service in my ResultObject. How do i inject this service to my ResultObject?
Thanks!
You are not using your service inside a result object. your factory is generating the result object.
You can define your factory service in services.yml of your bundle as:
result.factory:
class: ResultFactory
arguments: ["#translator"]
And in your controller you can call the service:
$resultObject = $this->get('result_factory')->get($type, $param);
Also you have core example how to create factory service using symfony2 in [the docs].(http://symfony.com/doc/current/components/dependency_injection/factories.html)

Resources