I can't for the life of me figure out how to handle a regular deserialization. I've read dozens of SO questions and also the official doc and it seems to be easy. Seems.
I've got a simple JSON, like:
[{"id":"00112063002463454431","first_name":"John","last_name":"Doe","date_of_birth":"2006-09-28"}]
Now I'd like to map it to my class Person. No matter what I've tried, it always complains about date_of_birth to be string. It is expected to be DateTimeInterface when the routine internally calls setDateOfBirth(?DateTimeInterface $dateOfBirth) inside the Person class. But in my understanding DateTimeNormalizer's denormalize() should've already converted it to a DateTime object before it hydrates the Person object, shouldn't it?
Inside my class the field is defined as follows:
#[ORM\Column(type: Types::DATE_MUTABLE)]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeInterface $dateOfBirth = null;
Deserializing process:
$serializer = new Serializer(
[new DateTimeNormalizer(), new GetSetMethodNormalizer(), new ArrayDenormalizer()],
[new JsonEncoder()]
);
$personsFromJson = $serializer->deserialize($requestContent, 'App\Entity\Person[]', 'json');
Is there anything else to do?!
Edit
One-class example:
<?php
namespace App\Controller;
use DateTimeInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;
class TestController extends AbstractController
{
#[Route(path: '/test', name: 'app_test')]
public function index(): Response
{
$json = '{"dateOfBirth":"2023-01-31"}';
$serializer = new Serializer(
[new DateTimeNormalizer(), new GetSetMethodNormalizer()],
[new JsonEncoder()]
);
$testPersonFromJson = $serializer->deserialize($json, TestPerson::class, 'json');
return $this->json($testPersonFromJson);
}
}
class TestPerson {
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeInterface $dateOfBirth = null;
public function getDateOfBirth(): ?DateTimeInterface {
return $this->dateOfBirth;
}
public function setDateOfBirth(?DateTimeInterface $dateOfBirth): self {
$this->dateOfBirth = $dateOfBirth;
return $this;
}
}
App\Controller\TestPerson::setDateOfBirth(): Argument #1
($dateOfBirth) must be of type ?DateTimeInterface, string given,
called in
[...]\vendor\symfony\serializer\Normalizer\GetSetMethodNormalizer.php
on line 163
The DateTimeNormalizer() gets instantiated, but indeed, denormalize() gets never called.
I had to add a ReflectionExtractor to my ObjectNormalizer. I didn't notice this anywhere.
new ObjectNormalizer(null, null, null, new ReflectionExtractor())
https://symfony.com/doc/current/components/property_info.html#reflectionextractor
Found via the comments after my 18567th search at: Symfony Serialize Entity with Datetime / Deserialize fails
Related
I'm trying to do a unit test in the environment as the title says. However, the method contained in the class under test requires an entity instance as an argument. So I'm trying to get the above entity in a generic homebrew test class that extends the TestCase class but I can't figure out how to do it.
I have little experience with functional testing. I used fixtures at that time, so I'm guessing that I should use fixtures this time too. Is that correct? I would appreciate if you could teach me how to do that as well.
Please let me know, even a little information.
below is the test class, and the interface of the test target.
ps. we are using nelmio/alice bundle.
<?php
declare(strict_types=1);
namespace xxx\xxxx\Tests\Customize\Server\AlladinOffice;
use Codeception\PHPUnit\TestCase;
use Customize\Service\AlladinOffice\CodeFormatter;
use Customize\Service\AlladinOffice\CustomerCode;
use Doctrine\Common\Persistence\ObjectManager;
use Eccube\Repository\CustomerRepository;
use PHPUnit\Framework\MockObject\MockObject;
use Proxies\__CG__\Eccube\Entity\Customer;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class CustomerCodeTest extends KernelTestCase
{
/**
* #var CustomerCode
*/
private $sut;
/**
* #var CustomerRepository|MockObject
*/
private $customerRepository;
protected function setUp()
{
$codeFormatter = new CodeFormatter();
$this->sut = new CustomerCode($codeFormatter);
$this->customerRepository = $this->createMock(CustomerRepository::class);
parent::setUp();
}
public function testGetCustomerCode()
{
$actual = $this->sut->getCustomerCode(null);
$expected = 1000000000;
$this->assertEquals($expected, $actual);
// $customer = $this->customerRepository->findBy([], ['id' => 'ASC']);
// dump($customer);exit();
// $actual = $this->sut->getCustomerCode($customer);
$customer = new Customer;
//$customer->set
//$this->customerRepository
//->expects($this->any())
//->method('find')
//->willReturn($);
}
}
<?php
declare(strict_types=1);
namespace Customize\Service\AlladinOffice;
use Eccube\Entity\Customer;
interface CustomerCodeInterface
{
public function getCustomerCode(?Customer $customer): string;
public function getChannelCode(?Customer $customer): string;
public function getRankCode(?Customer $customer): string;
}
for example a detailed method to test.
public function getCustomerCode(?Customer $customer): string
{
if (!$customer) {
return self::DEFAULT_CUSTOMER_CODE;
}
$customerRank = $customer->getXxxCustomer()->getCustomerRank();
$customerChannel = $customerRank ? $customerRank->getCustomerChannel() : null;
if (!$customerChannel || !$customerChannel->isExportToAO()) {
return self::DEFAULT_CUSTOMER_CODE;
}
return $this->formatter->getCustomerCodeFromCustomerId($customer->getId());
}
I manage to get a filtered collection of my Note entities with API Platform, using the #ApiFilter(SearchFilter::class) annotation.
Now I want to convert the json response which is an hydra collection
Example :
{
"#context": "/api/contexts/Note",
"#id": "/api/notes",
"#type": "hydra:Collection",
"hydra:member": []
}
to an archive containing one file by Note and return its metadata.
Example :
{
"name": "my_archive.zip",
"size": 12000,
"nb_of_notes": 15
}
I want to keep the SearchFilter benefits. Is the Normalization the good way to go ?
How to declare the normalizer ? How to access the collection/array of Notes in my normalize() method ?
According to the documentation symfony custom_normalizer , you can create a custom normalizer for your Note entity (for example NoteNormalizer). In the supportsNormalization method your must precise that the normalizer will only affect your Note entity by providing Note entity class. So in the normalize method, you will get each item of your ArrayCollection of Note. If you want to be sure, you can make a dump to $data variable (dd($data)) inside this normalize method, and you will have the first element of you ArrayCollection.
that's how I tried to understand it.
namespace App\Serializer;
use App\Entity\Note;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class NoteNormalizer implements ContextAwareNormalizerInterface
{
private $normalizer;
public function __construct(ObjectNormalizer $normalizer) // Like in documentation you can inject here some customer service or symfony service
{
$this->normalizer = $normalizer;
}
public function normalize($topic, $format = null, array $context = [])
{
$data = $this->normalizer->normalize($topic, $format, $context);
$data['name'] = 'some name';
$data['size'] = 12000;
$data['nb_of_notes'] = 15;
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
return $data instanceof Note;
}
}
Or if you want you can use this command to generate it automatically :
php bin/console make:serializer:normalizer
And give the name : NoteNormalizer
Simply create a "collection Normalizer" :
note: works the same for vanilla symfony projects too.
namespace App\Serializer;
use App\Entity\Note;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
class NoteCollectionNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function supportsNormalization($data, $format = null, array $context = []): bool
{
if(!is_array($data) || (!current($data) instanceof Note)) {
return false;
}
return true;
}
/**
* #param Note[] $collection
*/
public function normalize($collection, $format = null, array $context = [])
{
// ...
}
}
I have this entity:
AppBundle\Entity\Ciudad
class Ciudad{
...
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\ComunidadAutonoma")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_ccaa", referencedColumnName="id")
* })
*/
private $ccaa;
....
public function getCcaa()
{
return $this->ccaa;
}
public function setCcaa(ComunidadAutonoma $ccaa)
{
$this->ccaa = $ccaa;
}
}
And the other entity is:
AppBundle\Entity\ComunidadAutonoma
class ComunidadAutonoma{
properties
getters
setters
}
In a controller, I get data from a form, and I´m triying to deserialize the data into a Ciudad entity, but is getting me allways the same error:
Expected argument of type "AppBundle\Entity\ComunidadAutonoma", "integer" given
In the form data I send to the action in the controller, the value of the comunidadautonoma is the id of the selected option in a combo:
{
parameters...
ccaa:7,
parameters...
}
In my controller I have this:
<?php
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use AppBundle\Entity\Ciudad;
class CiudadController extends Controller
{
public function procesarAction(Request $request)
{
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$this->serializer = new Serializer($normalizers, $encoders);
$ciudad= $this->serializer->deserialize($parametros['parametros'], Ciudad::class, 'json');
}
}
Am I missing something?Do I need any special configuration to deserializer an entity with a relation?
You dont have to do anything if you properly configured a type. While creating a Form Type for your entity please add class name to your type like:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Ciudad::class,
]);
}
And please use english naming for your projects.
First of all, since you are sending form data to your controller you could use Form Type classes to leverage all the power of the Symfony Form Component that will all this job for you.
Answering your specific question (and assuming you cannot/don't want to use Symfony Form Component) this error is absolutely expected. As you can see in your setCcaa function declaration inside Ciudad class:
public function setCcaa(ComunidadAutonoma $ccaa)
Because of the type-hinting (ComunidadAutonoma $ccaa) setCcaa function expects an argument of type ComunidadAutonoma. Now when Symfony serializer tries to denormalize your json object it calls setCcaa function with argument the ccaa value provided in your json (in your example is 7) which happens to be an integer. So Symfony complains that you provide an integer instead of ComunidadAutonoma type.
In order to solve this problem you have to create and use your own normalizer so that you can transform this integer to the corresponding entity object from your database. Something like this:
class EntityNormalizer extends ObjectNormalizer
{
/**
* Entity manager
* #var EntityManagerInterface
*/
protected $em;
public function __construct(
EntityManagerInterface $em,
?ClassMetadataFactoryInterface $classMetadataFactory = null,
?NameConverterInterface $nameConverter = null,
?PropertyAccessorInterface $propertyAccessor = null,
?PropertyTypeExtractorInterface $propertyTypeExtractor = null
) {
parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);
// Entity manager
$this->em = $em;
}
public function supportsDenormalization($data, $type, $format = null)
{
return strpos($type, 'App\\Entity\\') === 0 && (is_numeric($data) || is_string($data));
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->em->find($class, $data);
}
}
What this normalizer does is that it checks if your data type (in this case $ccaa) is type of an entity and if the data value provided (in this case 7) is an integer, it transforms this integer to the corresponding entity object from your database (if existing).
To get this normalizer working you should also register it in your services.yaml configuration, with the appropriate tags like this:
services:
App\Normalizer\EntityNormalizer:
public: false
autowire: true
autoconfigure: true
tags:
- { name: serializer.normalizer }
You could also set the normalizer's priority but since the default priority value is equal to 0 when Symfony's built-in normalizers' priority is by default negative, your normalizer will be used first.
You could check a fully explained example of this in this fine article.
I have a controller that renders a form that is suppose to have a dropdown with titles mapped against a client_user entity. Below is code I use in my controller to create the form:
$builder = $this->get(form.factory);
$em = $this->get('doctrine.entity_manager');
$form = $builder->createBuilder(new ClientUserType($em), new ClientUser())->getForm();
Below is my ClientUserType class with a constructor that I pass the entity manager on:
<?php
namespace Application\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
class ClientUserType extends AbstractType
{
protected $entityManager;
public function __construct($entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', EntityType::class, array(
'class' => '\\Application\\Model\\Entity\\Title',
'em' => $this->entityManager
))
->add('name')
->add('surname')
->add('contact')
->add('email');
}
public function getName()
{
return 'client_user_form';
}
}
I keep on getting this catchable fatal error below and have no idea what I need to do in order to get a dropdown with titles from a database with doctrine.
Catchable fatal error: Argument 1 passed to Symfony\Bridge\Doctrine\Form\Type\DoctrineType::__construct() must be an instance of Doctrine\Common\Persistence\ManagerRegistry, none given, called in D:\web\playground-solutions\vendor\symfony\form\FormRegistry.php on line 90 and defined in D:\web\playground-solutions\vendor\symfony\doctrine-bridge\Form\Type\DoctrineType.php on line 111
Reading from that error I have no idea where I need to create a new instance of ManagerRegistry registry as it appears that the entity manager does not work. I am also thinking perhaps I need to get the ManagerRegistry straight from the entity manager itself.
Can someone please help explain the simplest way to get this to work? What could I be missing?
Seems that doctrine-bridge form component is not configured.
Add class
namespace Your\Namespace;
use Doctrine\Common\Persistence\AbstractManagerRegistry;
use Silex\Application;
class ManagerRegistry extends AbstractManagerRegistry
{
protected $container;
protected function getService($name)
{
return $this->container[$name];
}
protected function resetService($name)
{
unset($this->container[$name]);
}
public function getAliasNamespace($alias)
{
throw new \BadMethodCallException('Namespace aliases not supported.');
}
public function setContainer(Application $container)
{
$this->container = $container;
}
}
and configure doctrine-bridge form component
$application->register(new Silex\Provider\FormServiceProvider(), []);
$application->extend('form.extensions', function($extensions, $application) {
if (isset($application['form.doctrine.bridge.included'])) return $extensions;
$application['form.doctrine.bridge.included'] = 1;
$mr = new Your\Namespace\ManagerRegistry(
null, array(), array('em'), null, null, '\\Doctrine\\ORM\\Proxy\\Proxy'
);
$mr->setContainer($application);
$extensions[] = new \Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension($mr);
return $extensions;
});
array('em') - em is key for entity manager in $application
For others that may find this: If you want to use the EntityType and you're not using a framework at all, you need to add the DoctrineOrmExtension to your FormFactoryBuilder like so:
$managerRegistry = new myManagerRegistry(
'myManager',
array('connection'),
array('em'),
'connection',
'em',
\Doctrine\ORM\Proxy\Proxy::class
);
// Setup your Manager Registry or whatever...
$doctrineOrmExtension = new DoctrineOrmExtension($managerRegistry);
$builder->addExtension($doctrineOrmExtension);
When you use EntityType, myManagerRegistry#getService($name) will be called. $name is the name of the service it needs ('em' or 'connection') and it needs to return the Doctrine entity manager or the Doctrine database connection, respectively.
In your controller, try to call the service like that:
$em = $this->get('doctrine.orm.entity_manager');
Hope it will help you.
Edit:
Sorry, I thought you was on Symfony... I have too quickly read...
I am trying to set up a simple event subscription based on the example given here - http://symfony.com/doc/master/components/event_dispatcher/introduction.html.
Here's my event store:
namespace CookBook\InheritanceBundle\Event;
final class EventStore
{
const EVENT_SAMPLE = 'event.sample';
}
Here's my event subscriber:
namespace CookBook\InheritanceBundle\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\Event;
class Subscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
var_dump('here');
return array(
'event.sample' => array(
array('sampleMethod1', 10),
array('sampleMethod2', 5)
));
}
public function sampleMethod1(Event $event)
{
var_dump('Method 1');
}
public function sampleMethod2(Event $event)
{
var_dump('Method 2');
}
}
Here's the config in services.yml:
kernel.subscriber.subscriber:
class: CookBook\InheritanceBundle\Event\Subscriber
tags:
- {name:kernel.event_subscriber}
And here's how I raise the event:
use Symfony\Component\EventDispatcher\EventDispatcher;
use CookBook\InheritanceBundle\Event\EventStore;
$dispatcher = new EventDispatcher();
$dispatcher->dispatch(EventStore::EVENT_SAMPLE);
Expected output:
string 'here' (length=4)
string 'Method 1' (length=8)
string 'Method 2' (length=8)
Actual output:
string 'here' (length=4)
For some reason, the listener methods don't get called. Anyone knows what's wrong with this code? Thanks.
What #Tristan said. The tags portion in your services file is part of the Symfony Bundle and is only processed if you pull the dispatcher out of the container.
Your example will work as expected if you do this:
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new Subscriber());
$dispatcher->dispatch(EventStore::EVENT_SAMPLE);
You might try to inject a configured EventDispatcher (#event_dispatcher) instead of instanciating a new one (new EventDispatcher)
If you only create it and add an event-listener Symfony still has no reference to this newly created EventDispatcher object and won't use it.
If you are inside a controller who extends ContainerAware :
use Symfony\Component\EventDispatcher\EventDispatcher;
use CookBook\InheritanceBundle\Event\EventStore;
...
$dispatcher = $this->getContainer()->get('event_dispatcher');
$dispatcher->dispatch(EventStore::EVENT_SAMPLE);
I've adapted my answer thanks to this question's answer even though the context of both questions are different, the answer still applies.