How to inject Symfony Serializer to the controller properly? - symfony

I have a problem with injecting Symfony Serializer into the controller.
Here is a working example of what behavior I want to achive:
public function someAction(): Response
{
$goodViews = //here I get an array of Value Objects or DTO's ;
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
// normalize gives me a proper array for serialization
return $this->json(
$serializer->normalize($goodViews)
);
}
But now I need to change a direct creation of Serializer with dependency injection to controllers constructor or action. Other way, I think, is to create service which will get ObjectNormalizer and JsonEncoder as arguments, then create a Serializer and then normalize arrays of objects in special method and return result. But I can not figure out how to create serializer in service.yml or describe service dependencies properly. Symfony docs also get simple serializer service or create it manually as I did in the code example.
I thought to get serializer service from a service container in the action (with $this->get('serializer')) or inject it into the controllers constructor with NormalizerInterface (I need to normalize arrays of objects mostly), but this injected serializer fell with such an error:
"message": "Cannot normalize attribute \"options\" because the
injected serializer is not a normalizer", "class":
"Symfony\Component\Serializer\Exception\LogicException",
so I suppose, that its not configured in a way I've done with manual Serializer creation.
Our version of Symfony is 3.4 Thanks for your attention.

The decision of my problem is a little bit tricky. ObjectNormalizer was overriden by custom normalizer (with decorate part in the custom service defenition - see https://symfony.com/doc/3.4/service_container/service_decoration.html). Thats why in the framework preconfigured Symfony Serializer I got our custom one, which produced a mistake:
Cannot normalize attribute \"options\" because the injected serializer
is not a normalizer
So I've created a new serializer service with ObjectNormalizer:
new_api.serializer_with_object_normalizer:
class: Symfony\Component\Serializer\Serializer
arguments:
0:
- "#our_custom_serviec_which_override_object_normalizer.inner"
1:
- "#serializer.encoder.json"

public function someAction(SerializerInterface $serializer): Response // Add serializer as an argument
{
$goodViews = //here I get an array of Value Objects or DTO's ;
// normalize gives me a proper array for serialization
return $this->json(
$serializer->normalize($goodViews)
);
}
In services.yml
# *Bundle/Resources/services.yaml
services:
YourNamespace/*Bundle/Controller/YourController:
tags: [ 'controller.service_arguments' ]
Try it and let us know. This should get you going, but there are better ways you can configure your project. Check here for more info on using controllers as services and how to autowire them.
https://symfony.com/doc/3.4/service_container.html
https://symfony.com/doc/3.4/serializer.html

Related

How to extend FOSRestBundle RequestBodyParamConverter?

I am new to Symfony (5.3) and would like to extend the RequestBodyParamConverter (FOSRestBundle 3.0.5) to create a REST api. Using #ParamConverter annotation with the RequestBodyParamConverter works fine. However, I would like to create a custom converter, which does the exact same job as RequestBodyParamConverter plus a little extra work.
My first guess was to simply extend RequestBodyParamConverter and provide my custom subclass in the #ParamConverter annotation. However, RequestBodyParamConverter is defined as final and thus cannot be extended...
Injecting RequestBodyParamConverter / fos_rest.request_body_converter into a custom converter class (see example below) also fails because the service cannot be found. I assume this is because it is defined a private?
So, my last idea was to create a RequestBodyParamConverter inside my custom converter class. While this works, I am not sure if this is the right way to solve this problem. This way RequestBodyParamConverter is created twice. This is nothing special of course, but is this the Symfony way to solve this or are there other solutions?
Example:
Inject RequestBodyParamConverter in custom converter class
class MyParamConverter implements ParamConverterInterface {
protected $parentConverter;
public function __construct(ParamConverterInterface $parentConverter) {
$this->parentConverter = $parentConverter;
}
public function apply(Request $request, ParamConverter $configuration): bool {
doExtraWork();
return $this->parentConverter->apply(...);
}
}
// config/services.yaml
My\Project\MyParamConverter:
tags:
- { name: request.param_converter, converter: my_converter.request_body }
arguments:
# both fails since service is not found
$parentConverter: '#FOS\RestBundle\Request\RequestBodyParamConverter'
# OR
$parentConverter: '#fos_rest.request_body_converter'
Create RequestBodyParamConverter in custom converter class
class MyParamConverter implements ParamConverterInterface {
protected $parentConverter;
public function __construct(...parameters necessary to create converter...) {
$this->parentConverter = new RequestBodyParamConverter(...);
}
...
}
Symfony provide a way to decorate a registered service
To use it you need the FOS service id registered in the container.
To get it you can use this command
symfony console debug:container --tag=request.param_converter
Retrieve the Service ID of the service you want to override.
Then you can configure your service to decorate FOS one
My\Project\MyParamConverter:
decorates: 'TheIdOf_FOS_ParamConverterService'
arguments: [ '#My\Project\MyParamConverter.inner' ] # <-- this is the instance of fos service
Maybe you'll need to add the tags to this declaration, I'm not sure.
Let me know if you're facing an error.

symfony serializer jmsserializerbundle service name conflict

I have the following issue: I am working on a symfony (2.8) project which depends on the jmsserializerbundle (1.1).
When enabling the symfony-serializer alongside the jms-serializer package,
# app/config/config.yml
framework:
# ...
serializer: { enabled: true }
jms_serializer:
metadata:
#...
upon calling $this->get('serializer') or $this->get('jms_serializer') I only get the jms-serializer. This issue seems to have been resolved in jmsserializerbundle version 2.0: https://github.com/schmittjoh/JMSSerializerBundle/issues/558
Is there any way to solve this without updating jmsserializerbundle to 2.0?
Would there be any difference in performance compared to the normal symfony-serializer configuration, when wrapping a symfony-serializer in a custom service? like so:
<?php
use SomeCustomNormalizer;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
class SerializerService implements SerializerInterface
{
private $serializer;
public function __construct()
{
$this->serializer = new Serializer(
[new SomeCustomNormalizer(), new ObjectNormalizer()],
[new JsonEncode()]
);
}
public function serialize($data, $format, array $context = array())
{
# serialize
}
public function deserialize($data, $type, $format, array $context = array())
{
# deserialize
}
}
# SomeBundle/Resources/config/services.yml
serializer_service:
class: SomeBundle\SerializerService
The question regarding the performance came up for me because the existing jms configuration registers the jmsserializerbundle in the app kernel, which is not the case my custom service, which is just set up in services.yml.
Thanks in advance
Solution
As described below I just had to add one line to the jms-config:
# app/config/config.yml
jms_serializer:
enable_short_alias: false
metadata:
#...
Is there any way to solve this without updating jmsserializerbundle to 2.0?
JMS Serializer provides the option:
jms_serializer:
enable_short_alias: false
Would there be any difference in performance compared to the normal symfony-serializer configuration when wrapping a Symfony-serializer in a custom service? like so:
I guess not, the Symfony serializer is just 'another' service defined by the FrameworkBundle, a wrapper around the Serializer class with the normalizers and encoders injected.
If you create your own service (like in your example) it will be compiled by the service container as well. You can check the definition here: https://github.com/symfony/symfony/blob/v2.8.52/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml

Getting a list of tagged services in my controller

What i want is to add services to the service container that i want to use later in my controller or service.
So i created two services with my custom tag fbeen.admin
here they are:
services:
app.test:
class: AppBundle\Admin\TestAdmin
tags:
- { name: fbeen.admin }
fbeen.admin.test:
class: Fbeen\AdminBundle\Admin\TestAdmin
tags:
- { name: fbeen.admin }
Now i want to use all the services with the tag fbeen.admin in my controller but i dont know how.
I followed the How to work with service tags tutorial but i get stuck on this rule:
$definition->addMethodCall('addTransport', array(new Reference($id)));
On some way the addTransport method of the TransportChain class should be called but it seems that it isn't been called.
And even if it would be called then i still do not have a list of services with the fbeen.admin tag into my controller.
I am sure that i am missing something but who can explain me what it is?
p.s. I know that compilerPass runs at buildtime but for example sonata admin knows all admin classes and twig knows all twig extensions. How do they know?
Thank you for reading this :-)
Symfony 3.3
Container gets compiled once (in debug more often, but in production only once). What you manage with addMethodCall... is that once you request your service from container, which you are storing in $definition (that in this case is controller). Then container will call method addMethodCall('method'.. during initialising your service.
What it will look in container:
// This is pseudo content of compiled container
$service = new MyController();
// This is what compiler pass addMethodCall will add, now its your
// responsibility to implement method addAdmin to store admins in for
// example class variable. This is as well way which sonata is using
$service->addAdmin(new AppBundle\Admin\TestAdmin());
$service->addAdmin(new AppBundle\Admin\TestAdmin());
return $service; // So you get fully initialized service
Symfony 3.4+
What you can do is this:
// Your services.yaml
services:
App/MyController/WantToInjectSerivcesController:
arguments:
$admins: !tagged fbeen.admin
// Your controller
class WantToInjectSerivcesController {
public function __construct(iterable $admins) {
foreach ($admins as $admin) {
// you hot your services here
}
}
}
Bonus autotagging of your services. Lets say all your controllers implements interface AdminInterface.
// In your extension where you building container or your kernel build method
$container->registerForAutoconfiguration(AdminInterface::class)->addTag('fbeen.admin');
This will tag automatically all services which implement your interface with tag. So you don't need to set tag explicitly.
The thing to note here is this: The CompilerPass doesn't run the 'addTransport' (or whatever you may call it) in the compiler-pass itself - just says 'when the time is right - run $definition->addTransport(...) class, with this data'. The place to look for where that happens is in your cache directory (grep -R TransportChain var/cache/), where it sets up the $transportChain->addTransport(...).
When you come to use that service for the first time - only then is the data filled in as the class is being instantiated from the container.
This worked for me:
extend the TransportChain class with a getTransports method:
public function getTransports()
{
return $this->transports;
}
and use the TransportChain service in my controller:
use AppBundle\Mail\TransportChain;
$transportChain = $this->get(TransportChain::class);
$transports = $transportChain->getTransports();
// $transports is now an array with all the tagged services
Thank you Alister Bulman for pushing me forwards :-)

Symfony 3 - How inject own variable from config to service right way?

I want on my web create cache config with different cache values. I have working example:
// config.yml
parameters:
myValue:
first: 1
second: 2
// services.yml
my_repo:
class: AppBundle\Repository\MyRepository
factory: ["#doctrine.orm.entity_manager", getRepository]
arguments:
- 'AppBundle\Entity\My'
calls:
- [setValue, ["%myValue%"]]
// MyRepository.php
public function setValue($val) {
$this->first = $val['first'];
}
// Inside controller method
$someVariable = $this->get('my_repo')
->someOtherFunction();
But is this way correct? What if another programmer will call repository 'standart' way $em->getRepository('MyRepository')? It will crash on udefined variable... Is there way to do this for example via constructor? Or constructor is bad idea?
I am interested in the yours practice - better solution etc.
Something like
[setValue, ["#=container.hasParameter('myValue') ? parameter('myValue') : array()"]]
Should do the trick. Then just check in your service if the variable injected is empty or not. See the doc for more on the Expression language

The right way of using a pagination class in Symfony

I'm trying to use this Symfony bundle:
https://github.com/KnpLabs/KnpPaginatorBundle
In the docs, they use it a controller. So they have easy access to service container or the request object.
But as far as I understand, the Doctrine query should be in a repository, not a controller, right? And I already do have a function returning records. It's just that the pagination service doesn't expect "results" upon instantiating. It wants the query. So I can't return the "results" to the controller, but rather in middle of this function use a paginator.
On the other hand, stuff like playing with services or requests indeed belong to controllers.
So how this should be done? At first I thought about injecting the "knp_paginator" service and the request object into the repository. But I don't think this is the right way.
I'd say that the Request object should not go further down the stack than from the Controller.
Nothing prevents you from injecting the paginator directly into your custom repository, so why not doing that?
your.repository.service.definition:
class: Your\Repository\Class
# for symfony 2.3
factory_service: doctrine
factory_method: getRepository
# for symfony 2.8 and higher
factory: ["#doctrine.orm.entity_manager", getRepository]
arguments:
- YourBundle:YourEntity
calls:
- [setPaginator, ["#knp_paginator"]]
In the repository, you then should have the paginator available for use with the QueryBuilder:
public function setPaginator($paginator)
{
$this->paginator = $paginator;
}
...
$this->paginator->paginate($qb->getQuery(), $page, $limit);
In order to get your $page and $limit variables into the repository, you don't need the Request object. Simply pass them as a parameter to the repository call:
// In your controller
// You can use forms here if you want, but for brevity:
$criteria = $request->get('criteria');
$page = $request->get('page');
$limit = $request->get('limit');
$paginatedResults = $myCustomRepository->fetchPaginatedData($criteria, $page, $limit);
Passing the request object further down the Controller means that you have a leak in your abstractions. It's no concern of your application to know about the Request object. Actually, the request might well come from other sources such as the CLI command. You don't want to be creating a Request object from there because of a wrong level of abstraction.
Assuming that you have a Custom Repository Class, you can have a method in that Repository, which returns a Query or a valid instance of Query Builder and then you call that method from the controller and pass it to the paginate() method.
For example where $qb is returned by the custom repository (not return result but just the querybuilder of it)
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$qb->getQuery(),
$request->query->getInt($pageParameterName, 1),
$perPage,
array('pageParameterName' => $pageParameterName)
);

Resources