How to globally define configuration of Serializer used in API Platform - symfony

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

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.

How to override translator in symfony 5.2

I'm trying to override translator class in Symfony 5.2. I tried this:
# config/services.yaml
services:
# ....
App\Translator:
decorates: translator
and this (App\Translator implements TranslatorInterface):
# config/services.yaml
services:
# ....
App\Translator:
arguments:
$translator: '#translator'
Symfony\Contracts\Translation\TranslatorInterface: '#App\Translator'
both methods work well in PHP code, but in development mode in the twig, translator service is still DataCollectorTranslator. So in twig templates the translator service remains not overridden. How can I fix it?
It's possible I am not understanding the question. If something works in one mode but not another then sometimes just deleting the var/cache directory and building a new cache with bin/console cache:clear might work.
Decorating services can be a bit interesting sometimes. I created a fresh 5.2 project and then added:
# src/Translation/Translation.php
namespace App\Translation;
use JetBrains\PhpStorm\Pure;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Bundle\FrameworkBundle\Translation\Translator as BaseTranslator;
class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
{
// Uses PHP8 constructor promotion
public function __construct(private BaseTranslator $translator)
{
}
#[Pure]
public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string
{
//return $this->translator->trans($id,$parameters,$domain,$locale);
return strtoupper($id); // Verify calling this class
}
public function getCatalogue(string $locale = null): MessageCatalogueInterface
{
return $this->translator->getCatalogue($locale);
}
#[Pure]
public function getLocale(): string
{
return $this->translator->getLocale();
}
public function setLocale(string $locale)
{
$this->translator->setLocale($locale);
}
}
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\Translation\Translator:
decorates: translator
# index.html.twig
<li>{{ 'Hello' | trans }}</li>
You can disregard the Pure stuff as well as some of the PHP8 stuff. I was using this as a PHP8 test as well.
But it all seems to work as advertised.

Dynamically get an instance of a service in controller by some value

I want to get a service instance in controller (symfony 4) just by value that might look like this:
$provider = 'youtube'
That's my setup:
Class videoProvider {
//Here I have all common methods for all services
}
Class YoutubeService extends videoProvider {}
Class OtherVideoService extends videoProvider {}
Now the question is how to do something like this. If $provider = 'youtube'
Then use YouTube service new YoutubeService () and go on. But what if service does not exist? What then?
Is that even possible?
You can do something like this
Create a folder with the name Provider
Create an interface, for example, VideoProviderInterface, and put into the folder
To your interface add the method getProviderName(): string
Create your providers and put them into the folder and implement the interface
To your services.yaml add the _instanceof: option, and add to your interface some tag
Exclude your provider folders from the App\: option in the services.yaml
Create class, ProviderManager and inject your service locator
More information you can find here
services.yaml
_instanceof:
App\Provider\VideoProviderInterface:
tags: ['app.provider']
App\Provider\:
resource: '../../src/Provider/*'
App\Provider\ProviderManager:
arguments:
- !tagged_locator { tag: 'app.provider', default_index_method: 'getProviderName' }
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Provider}'
VideoProviderInterface
<?php
namespace App\Provider;
interface VideoProviderInterface
{
public function getProviderName(): string
}
ProviderManager
<?php
namespace App\Provider;
use Symfony\Component\DependencyInjection\ServiceLocator;
class ProviderManager
{
/**
* #var ServiceLocator
*/
private $serviceLocator;
public function __construct(ServiceLocator $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
public function findByName(string $name): ?VideoProviderInterface
{
return $this->serviceLocator->has($name) ? $this->serviceLocator->get($name) : null;
}
}
$this->container->has('my-service-name') and $this->container->get('my-service-name') in a controller is probably what you are looking for. The my-service-name is the name you give the service in your service config and make sure your service is public.
Exemple config (see doc here)
# this is the service's name
site_video_provider.youtube:
public: true
class: App\Provider\YoutubeService
[...]
Then in a controller, or any container aware classes: (see doc here)
$service_name = 'site_video_provider.'.$provider;
if($this->container->has($service_name)){
$service = $this->container->get($service_name);
}

Supply validation group context with Symfony / API Platform

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!

Access Doctrine within a custom service in Symfony4

Aware that there is a lot of information around the net regarding this, I am still having a lot of trouble getting this to work.
I have created a custom service:
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\AccommodationType;
use App\Entity\Night;
class AvailabilityChecks {
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function nightAvailable(string $RoomCode, string $NightDate) {
$GetRoom = $this->em->getDoctrine()->getRepository(AccommodationType::class)->findOneBy([
'RoomCode' => $RoomCode
]);
$RoomQnt = $GetRoom->getNightlyQnt();
$GetNight = $this->em->getDoctrine()->getRepository(Night::class)->findOneBy([
'RoomCode' => $RoomCode,
'NightDate' => $NightDate
]);
$NumberOfNights = $GetNight->count();
if($NumberOfNights<$RoomQnt) {
return true;
}
else {
return false;
}
}
}
and have put this in services.yaml:
AvailabilityChecks.service:
class: App\Service\AvailabilityChecks
arguments: ['#doctrine.orm.entity_manager']
So when I try and use this in my controller, I get this error:
Too few arguments to function App\Service\AvailabilityChecks::__construct(), 0 passed in /mypath/src/Controller/BookController.php on line 40 and exactly 1 expected
I just can't figure out why it's not injecting the ORM stuff into the constructor! Any help greatly appreciated
The problem is in your BookController. Even though you didn't posted its code I can assume you create new AvailabilityChecks in it (on line 40).
In Symfony every service is intantiated by service container. You should never intantiate service objects by yourself. Instead BookController must ask service container for AvailabilityChecks service. How should it do it ?
In Symfony <3.3 we used generally :
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller
{
public function myAction()
{
$em = $this->get('doctrine.orm.entity_manager');
// ...
}
}
Nowadays services can be injected in controllers using autowiring which is way easier:
use Doctrine\ORM\EntityManagerInterface;
class MyController extends Controller
{
public function myAction(EntityManagerInterface $em)
{
// ...
}
}
You are using the wrong service for what you want to do. The alias doctrine that is used, e.g. in the AbstractController when you call getDoctrine() is bound to the service Doctrine\Common\Persistence\ManagerRegistry.
So the code you wrote fits better with that and you should either add #doctrine or #Doctrine\Common\Persistence\ManagerRegistry to the service definition.
Both with your current configuration or the changed one, you don't have to call $this->em->getDoctrine(), because $this->em is already equivalent to $this->getDoctrine() from your controller. Instead you could create a (private) method to make it look more like that code, e.g.:
private function getDoctrine()
{
return $this->em;
}
Then you can call $this->getDoctrine()->getRepository(...) or use $this->em->getRepository(...) directly.
In Symfony 4, you dont need to create it as services. This is automatically now. Just inject the dependencies what you need in the constructor. Be sure that you have autowire property with true value in services.yml (it is by default)
Remove this from services.yml:
AvailabilityChecks.service:
class: App\Service\AvailabilityChecks
arguments: ['#doctrine.orm.entity_manager']
You dont need EntityManagerInterface because you are not persisting anything, so inject repositories only.
<?php
namespace App\Service;
use App\Entity\AccommodationType;
use App\Entity\Night;
use App\Repository\AccommodationTypeRepository;
use App\Repository\NightRepository;
class AvailabilityChecks {
private $accommodationTypeRepository;
private $nightRepository
public function __construct(
AcommodationTypeRepository $acommodationTypeRepository,
NightRepository $nightRepository
)
{
$this->acommodationTypeRepository = $acommodationTypeRepository;
$this->nightRepository = $nightRepository;
}
public function nightAvailable(string $RoomCode, string $NightDate) {
$GetRoom = $this->acommodationTypeRepository->findOneBy([
'RoomCode' => $RoomCode
]);
$RoomQnt = $GetRoom->getNightlyQnt();
$GetNight = $this->nightRepository->findOneBy([
'RoomCode' => $RoomCode,
'NightDate' => $NightDate
]);
$NumberOfNights = $GetNight->count();
if($NumberOfNights<$RoomQnt) {
return true;
}
else {
return false;
}
}
}
In SF4, you no longer need to specify dependencies required by your custom service in the service.yaml file. All you have to do is to use dependency injection.
So remove config lines, and call your service directly in the controller method :
<?php
namespace App\Controller;
use App\Service\AvailabilityChecks ;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class AppController extends AbstractController
{
public function index(AvailabilityChecks $service)
{
...
}
}
Having said that, i think you don't need custom service to do simple operations on database. Use repository instead.

Resources