How to configure Normalizer with injected SerializerInterface - php

I know that I can use and configure e.g. the DateTimeNormalizer of Symfony's Serializer like that:
$serializer = new Serializer(array(new DateTimeNormalizer()));
$dateAsString = $serializer->normalize(
new \DateTime('2016/01/01'),
null,
array(DateTimeNormalizer::FORMAT_KEY => 'Y/m')
);
// $dateAsString = '2016/01';
But if I want to use the Serializer without instantiation but with the "full Symfony app" Dependency injection instead like:
//class MyService...
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
//function MyFunction()
{
$json = $this->serializer->serialize($object, 'json');
}
it already comes with the most common Normalizers in a pre-ordered fashion.
Usually this is perfectly fine for my purpose.
But how could I e.g. configure the DateTimeNormalizer::FORMAT_KEY in the injection scenario without creating CustomNormalizers or lots of instantiating?

I just finished a sprint of my project with a similar problem.
If you only want to format by default you DateTime object, you can add those lines in your config file:
services:
serializer.normalizer.datetime:
class: ‘Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
arguments
-
!php/const Symfony\Component\Serializer\Normalizer\DateTimeNormalizer::FORMAT_KEY: 'Y-m-d\TH:i:s.uP’
tags:
- { name: serializer.normalizer, priority: -910 }
But in case you want more flexibility and control over the serializer, i advise you to not modify config file but try this following method :
So for my needs, i finally create a MainController that extends AbstractController and each of my controllers extends MainController.
In the MainController I instanciate the serializer as property, so that you can access serializer already configured in the MainControlle by $this->serializer.
MainController :
class MainController extends AbstractController {
protected $serializer;
public function __construct() {
$encoders = [new XmlEncoder(), new JsonEncoder()];
$normalizers = [
new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => "your date format"]),
new ObjectNormalizer()
];
$this->serializer = new Serializer($normalizers, $encoders);
}
}
PostController :
class PostController extends MainController {
/**
* #Route("/posts")
**/
public function listPosts() {
$e = new Entity();
return $this->serializer->serialize($e, "json");
}
}
EDIT : !! I forgot to say, be careful to the order you arrange normalizer in instanciation. As DateTime is an object, the serializer will try to normalize the DateTime object with ObjectNormalizer first if ObjectNormalizer is added first (which is used to normalize entities) and will return an empty array.

Since symfony 5.3, you could add the context as a inline metadata.
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class SomeClass
{
/**
* #Serializer\Context({ DateTimeNormalizer::FORMAT_KEY = 'Y-m-d' })
*/
public \DateTime $date;
// In PHP 8 applications you can use PHP attributes instead:
#[Serializer\Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public \DateTime $date;
}
Read more about it in the blog post:
https://symfony.com/blog/new-in-symfony-5-3-inlined-serialization-context

Related

New alternative for getDoctrine() in Symfony 5.4 and up

As my IDE points out, the AbstractController::getDoctrine() method is now deprecated.
I haven't found any reference for this deprecation neither in the official documentation nor in the Github changelog.
What is the new alternative or workaround for this shortcut?
As mentioned here:
Instead of using those shortcuts, inject the related services in the constructor or the controller methods.
You need to use dependency injection.
For a given controller, simply inject ManagerRegistry on the controller's constructor.
use Doctrine\Persistence\ManagerRegistry;
class SomeController {
public function __construct(private ManagerRegistry $doctrine) {}
public function someAction(Request $request) {
// access Doctrine
$this->doctrine;
}
}
You can use EntityManagerInterface $entityManager:
public function delete(Request $request, Test $test, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$test->getId(), $request->request->get('_token'))) {
$entityManager->remove($test);
$entityManager->flush();
}
return $this->redirectToRoute('test_index', [], Response::HTTP_SEE_OTHER);
}
As per the answer of #yivi and as mentionned in the documentation, you can also follow the example below by injecting Doctrine\Persistence\ManagerRegistry directly in the method you want:
// src/Controller/ProductController.php
namespace App\Controller;
// ...
use App\Entity\Product;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
/**
* #Route("/product", name="create_product")
*/
public function createProduct(ManagerRegistry $doctrine): Response
{
$entityManager = $doctrine->getManager();
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(1999);
$product->setDescription('Ergonomic and stylish!');
// tell Doctrine you want to (eventually) save the Product (no queries yet)
$entityManager->persist($product);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new product with id '.$product->getId());
}
}
Add code in controller, and not change logic the controller
<?php
//...
use Doctrine\Persistence\ManagerRegistry;
//...
class AlsoController extends AbstractController
{
public static function getSubscribedServices(): array
{
return array_merge(parent::getSubscribedServices(), [
'doctrine' => '?'.ManagerRegistry::class,
]);
}
protected function getDoctrine(): ManagerRegistry
{
if (!$this->container->has('doctrine')) {
throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
}
return $this->container->get('doctrine');
}
...
}
read more https://symfony.com/doc/current/service_container/service_subscribers_locators.html#including-services
In my case, relying on constructor- or method-based autowiring is not flexible enough.
I have a trait used by a number of Controllers that define their own autowiring. The trait provides a method that fetches some numbers from the database. I didn't want to tightly couple the trait's functionality with the controller's autowiring setup.
I created yet another trait that I can include anywhere I need to get access to Doctrine. The bonus part? It's still a legit autowiring approach:
<?php
namespace App\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Symfony\Contracts\Service\Attribute\Required;
trait EntityManagerTrait
{
protected readonly ManagerRegistry $managerRegistry;
#[Required]
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
{
// #phpstan-ignore-next-line PHPStan complains that the readonly property is assigned outside of the constructor.
$this->managerRegistry = $managerRegistry;
}
protected function getDoctrine(?string $name = null, ?string $forClass = null): ObjectManager
{
if ($forClass) {
return $this->managerRegistry->getManagerForClass($forClass);
}
return $this->managerRegistry->getManager($name);
}
}
and then
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\Foobar;
class SomeController extends AbstractController
{
use EntityManagerTrait
public function someAction()
{
$result = $this->getDoctrine()->getRepository(Foobar::class)->doSomething();
// ...
}
}
If you have multiple managers like I do, you can use the getDoctrine() arguments to fetch the right one too.

Use parameters in Symfonybundle in service class

I have Symfony bundle called upload images:
I want to use parameters in my bundle in my class.
This is my parameter file:
upload-images:
image:
crop_size: 300
My files:
Configuration.php
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('upload-images');
$treeBuilder->getRootNode()
->children()
->arrayNode('image')
->children()
->integerNode('save_original')->end()
->scalarNode('crop_size')->end()
->end()
->end() // twitter
->end();
return $treeBuilder;
}
}
UploadImagesExtension.php
class UploadImagesExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources'));
$loader->load('services.yaml');
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
}
}
And final my service class:
Rotate.php
And in this class I want the parameter: crop_size
I tried the ParameterBagInterface:
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class Rotate
{
private $params;
public function __construct(ParameterBagInterface $params)
{
$this->params = $params;
}
public function Rotate()
{
$cropSize = $params->get('crop_size');
}
}
UserController.php
use verzeilberg\UploadImagesBundle\Service\Rotate;
class UserController extends AbstractController
{
/** #var UserProfileService */
private $service;
private $userService;
public function __construct(
UserProfileService $service,
UserService $userService
) {
$this->service = $service;
$this->userService = $userService;
}
/**
* #param UserInterface $user
* #return Response
*/
public function profile(UserInterface $user)
{
$rotate = new Rotate();
$rotate->Rotate();
.....
...
}
Getting this error:
Too few arguments to function verzeilberg\UploadImagesBundle\Service\Rotate::__construct(), 0 passed in /home/vagrant/projects/diabetigraph-dev/src/Controller/User/UserController.php on line 62 and exactly 1 expected
I have search for a solution. But did not came accross the right one.
According to the latest edit, the error is pretty obvious: if you want to use dependency injection, you have to use it. Calling $rotate = new Rotate(); without any constructor parameters will fail, as Symfony cannot inject them for you.
Instead, inject it through the action:
public function profile(UserInterface $user, Rotate $rotate)
... this will use Symfony's container and inject the ParameterBagInterface, if you have enabled autowiring. If not, you have to write the proper service definitions to get this done
The error you are getting is not directly related to your question.
Either it is an autowiring issue, or maybe you are trying to instanciate the service manually? Please share what you are doing here: UserController.php on line 62
Anyway, to answer your question:
To access the parameters from the parameter bag you will have to set them in the extension.
$container->setParameter('my_bundle.config', $config);
Also, injecting the whole ParameterBag is fine for a project, but should be avoided for a bundle.
Use the DI config to inject just your parameter, OR make your extension implement CompilerPassInterface, and override the definition there. (It may be overkill for such a simple task)

Fixture object reference incorrect in functional test

A functional test class relies on an object reference created in a fixture. The reference's id, however, is not identical to the object's id property as returned by the entity manager. Below is a test that demonstrates this problem.
Notes:
The error is the same when using $this->setReference(...) as when
using the public const ... and $this->addReference(...).
The object reference used in the test appears to be the next
available id for nonprofit entities.
The test class was created after the error was observed in a more general test class.
The error is the same whether or not the fixtures are loaded before
running the test class.
The application uses Symfony 5.1.2 with all dependencies updated.
Test class:
namespace App\Tests\Controller;
use Liip\TestFixturesBundle\Test\FixturesTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ReferenceTest extends WebTestCase
{
use FixturesTrait;
public function setup(): void {
$this->client = $this->createClient();
$this->fixtures = $this->loadFixtures([
'App\DataFixtures\Test\OptionsFixture',
'App\DataFixtures\Test\NonprofitFixture',
'App\DataFixtures\Test\OpportunityFixture',
'App\DataFixtures\Test\UserFixture',
])
->getReferenceRepository();
$this->client->followRedirects();
$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager('test');
}
public function testNonprofitReference() {
$npo = $this->entityManager->getRepository(\App\Entity\Nonprofit::class)
->findOneBy(['orgname' => 'Marmot Fund']);
$nId = $npo->getId();
$id = $this->fixtures->getReference('npo')->getId();
$this->assertEquals($nId, $id, 'Reference incorrect');
}
}
Test result:
Reference incorrect
Failed asserting that 4 matches expected 1.
NonprofitFixture (other fixtures may not be relevant):
namespace App\DataFixtures\Test;
use App\Entity\Nonprofit;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Bundle\FixturesBundle\ORMFixtureInterface;
class NonprofitFixture extends AbstractFixture implements OrderedFixtureInterface, ORMFixtureInterface
{
public const NPO_REFERENCE = 'npo';
public function load(ObjectManager $manager) {
$npo = new Nonprofit();
$npo->setOrgname('Marmot Fund');
$npo->setEin('123456789');
$npo->setActive(false);
// $this->setReference('npo', $npo);
$this->addReference(self::NPO_REFERENCE, $npo);
$npo1 = new Nonprofit();
$npo1->setOrgname('Turkey Fund');
$npo1->setEin('321654978');
$npo1->setActive(true);
$npo1->setWebsite('http://turkeysRUs.bogus.info');
$npo3 = new Nonprofit();
$npo3->setOrgname('Talk Trash Fund');
$npo3->setEin('978654321');
$npo3->setActive(true);
$npo3->setWebsite('http://ttrash.bogus.info');
$staff = $this->getReference(UserFixture::STAFF_REFERENCE);
$npo->setStaff($staff);
$opp = $this->getReference(OpportunityFixture::OPP_REFERENCE);
$opp1 = $this->getReference(OpportunityFixture::OPP1_REFERENCE);
$npo1->addOpportunity($opp);
$npo3->addOpportunity($opp1);
$manager->persist($npo);
$manager->persist($npo1);
$manager->persist($npo3);
$manager->flush();
}
public function getOrder() {
return 5; // the order in which fixtures will be loaded
}
}
framework.yaml excerpt:
liip_test_fixtures:
keep_database_and_schema: true
cache_db:
sqlite: liip_test_fixtures.services_database_backup.sqlite
dama_doctrine_test_bundle.yaml:
dama_doctrine_test:
enable_static_connection: true
enable_static_meta_data_cache: true
enable_static_query_cache: true
csv export from app.db:
"id","orgName"
"1","Marmot Fund"
"2","Turkey Fund"
"3","Talk Trash Fund"
The answer, such as it is, is that references have no place in a functional test. Their use is really a shortcut for clicking on links or taking some other action. A better test is to use the crawler to mimic the action.

Get request service from a DataTransformer class

Short story:
I need to get the Request service from a class that doesn't inherit from the Controller class (it's a DataTransformer which -obviously- implements the DataTransformerInterface).
Long story:
I have an embedded form that has an email field. If the user enters an email which doesn't exists in my users database table, I want to create a new user with this email.
In order to do that, I need to set its IP, so I followed the embedded forms tutorial and the data transformer recipe, but finally I have no idea where I'm able to inject the Request instance to my DataTransformer constructor or something else.
If I was in a class extending form the Controller one, it would be as simple as:
$this->container->get('request')->getClientIp()
You can do this by "Referencing (Injecting) Services". In your case you want to inject the Request which is a service from a narrower scope.
If you are using transformers, you are probably already using a Custom Form Type, and are instantiating the Data Transformer within your Form Type BuildForm Method, see here for more info.
You want to inject the Request object to the custom Form Type, then it can passed to the Data Transformer as a constructor parameter.
To do this modify the services.yml file with in your bundle, and add a constructor to the Custom Form Type and the Custom Data Transformer like this:
// src/Acme/HelloBundle/Resources/config/services.yml
parameters:
// ...
services:
acme.type.custom_type:
class: Acme\HelloBundle\Form\Type\CustomType
scope: request
arguments: ["#doctrine.odm.mongodb.document_manager", "#request"]
tags:
- { name: form.type, alias: custom_type }
The update the CustomType Class like this:
<?php
// src/Acme/HelloBundle/Form/Type/CustomType.php
namespace Acme\HelloBundle\Form\Type;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
use Acme\HelloBundle\Form\DataTransformer\CustomDataTransformer;
class CustomType extends AbstractType
{
private $request;
private $dm;
public function __construct(DocumentManager $dm, Request $request) {
$this->dm = $dm;
$this->request = $request;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Create a new Data Transformer that will be able to use the Request Object!
$transformer = new CustomDataTransformer($this->dm, $this->request);
$builder->addModelTransformer($transformer);
}
// ...
}
and finally add a constructor to the transformer similar to the one added in the Form Type:
<?php
// src/Acme/HelloBundle/Form/DataTransformer/CustomDataTransformer.php
namespace Acme\HelloBundle\Form\DataTransformer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\DataTransformerInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
class CustomDataTransformer implements DataTransformerInterface
{
private $request;
private $dm;
public function __construct(DocumentManager $dm, Request $request) {
$this->dm = $dm;
$this->request = $request;
}
// ...
}
Notice that along with the Request I have injected the MongoDB DocumentManager, this is to show that multiple objects can be injected.
Ok, that's simple:
In my question I was assuming that the DataTransformer will be "magically" invoked, but it's instanced while building the form, so if it helps to anyone, here it is:
In the DataTransformer class (implementing the DataTransformerInterface):
Define the new class attributes in order to hold the dependency injection:
/**
* #var EntityManager
*/
private $entityManager;
/**
* #var \Symfony\Component\DependencyInjection\Container
*/
private $container;
Define the constructor like:
public function __construct( EntityManager $entityManager, Container $container )
{
$this->entityManager = $entityManager;
$this->container = $container;
}
In your form class (implementing the AbstractType)
Add the following calls to the setDefaultOptions method:
$resolver->setRequired( array( 'em', 'container' ) );
$resolver->setAllowedTypes( array(
'em' => 'Doctrine\Common\Persistence\ObjectManager',
'container' => 'appDevDebugProjectContainer',
) );
In the buildForm method, apply the transformer as defined in the transformer recipe but instance it as:
$entityManager = $options['em'];
$container = $options['container'];
$transformer = new FantasticTransformer( $entityManager, $container );
In your controller, when you're calling to the createForm method, is it possible to inject the EntityManager and the Container instances simply adding them as follows:
$form = $this->createForm( 'your_form', $lookup, array(
'action' => $this->generateUrl( 'your_action_url' ),
'em' => $this->getDoctrine()->getManager(),
'container' => $this->container
) );
Now, you can finally get the client IP from the request service calling to the container defined in the constructor of your DataTransformer class:
$ip = $this->container->get('request')->getClientIp();
Note that we're injecting the container instead of the request instance, it's due to the Symfony scopes.

Zend framework 2 translator in model

How to get translator in model?
Inside view we can get translator using this code
$this->translate('Text')
Inside controller we can get translator using this code
$translator=$this->getServiceLocator()->get('translator');
$translator->translate("Text") ;
But how to get translator in model?
I'd tried so many ways to get service locator in models
2 of those
1)Using MVC events
$e=new MvcEvent();
$sm=$e->getApplication()->getServiceManager();
$this->translator = $sm->get('translator');
if i pring $sm it is showing null. but it works fine in Model.php onBootstrap
2)Created one model which implements ServiceLocatorAwareInterface
SomeModel.php
<?php
namespace Web\Model;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class SomeModel implements ServiceLocatorAwareInterface
{
protected $services;
public function setServiceLocator(ServiceLocatorInterface $locator)
{
$this->services = $locator;
}
public function getServiceLocator()
{
return $this->services;
}
}
and used that inside my model
$sl = new SomeModel();
$sm=$sl->getServiceManager();
var_dump($sm); exit;
$this->translator = $sm->get('translator');
this is also printing null.
If you don't need the servicemanager instance in your model, simply inject translator instance to it.
For example:
// Your model's constructor
class MyModel {
// Use the trait if your php version >= 5.4.0
use \Zend\I18n\Translator\TranslatorAwareTrait;
public function __construct( $translator )
{
$this->setTranslator( $translator );
}
public function modelMethodWhichNeedsToUseTranslator()
{
// ...
$text = $this->getTranslator()->translate('lorem ipsum');
// ...
}
}
When you creating your model first time on service or controller level
class someClass implements ServiceLocatorAwareInterface {
public function theMethodWhichCreatesYourModelInstance()
{
// ...
$sm = $this->getServiceLocator();
$model = new \Namespace\MyModel( $sm->get('translator') )
// ...
}
}
If you need to instantiate your model (new MyModel();) on multiple methods/classes, consider to writing a factory for it.
Here is a nice article about Dependency Injection and PHP by Ralph Schindler for more detailed comments about this approach.
For your Model class to be ServiceLocatorAware, you not only need to implement the interface, you also need to make your model a service of the service manager, and fetch the model from there.
Add your model to the service manager, since it doesn't appear to need any constructor params, it's invokable, so you can add it to the invokables array in service manager config. You can do that by using the getServiceConfig() method in your Module class...
class Module
{
public function getServiceConfig()
{
return array(
'invokables' => array(
'SomeModel' => 'Fully\Qualified\ClassName\To\SomeModel',
),
);
}
}
Then, instead of calling the new keyword to create your model instance, you fetch it from the service manager, for instance, by calling the getServiceLocator() method in a controller action...
public function fooAction()
{
$sm = $this->getServiceLocator();
$model = $sm->get('SomeModel');
}
When your model is fetched from the service manager, a service initializer will look to see if it implements the ServiceLocatorAwareInterface and automatically call setServiceLocator() if it does, passing it an instance of the service manager.

Categories