Symfony array of objects deserialization - php

I have this class:
use Symfony\Component\Uid\Ulid;
final class Payload
{
/**
* #param Ulid[] $ulidList
*/
public function __construct(
public readonly string $id,
public readonly array $ulidList,
) {
}
}
when serializing it
$this->serializer->serialize($payload, 'json');
I'm receiving this output:
{"id":"XXX","ulidList":["01GP9H0WPW2A2BK9GYV9GQJMAK"]}
but when de-serializing the above
$this->serializer->deserialize($data, Payload::class, 'json');
the $ulidList property is filled with array of strings instead of Ulid objects.
How to make it to fill it with Ulid? I'm using SerializerInterface loaded from Dependency Injection.

I ended up making my own instance of Serializer with this way:
$this->serializer = new Serializer(
normalizers: [
new UidNormalizer(),
new ArrayDenormalizer(),
new ObjectNormalizer(
propertyTypeExtractor: new PropertyInfoExtractor(
typeExtractors: [new PhpDocExtractor(), new ReflectionExtractor()],
),
),
],
encoders: [
new JsonEncoder(),
],
);
What I was missing is the ObjectNormalizer with properly filled propertyTypeExtractor and ArrayDenormalizer.

Related

Symfony Entity id is null in Unit Test

Hi I have a question i have a script that has this
public function add(Request $request): UserResponse
{
$user = new User();
/** #var $request UserRequest */
$user->setName($request->getName());
$user->setEmail($request->getEmail());
$this->dataService->addUpdate($user);
return new UserResponse(
$user->getId(),
$user->getName(),
$user->getEmail()
);
}
Now I want to Unit Test this function, but it gives me the error that $user->getId() is null instead of an int (the UserResponse() want the first parameter to be int and not null)
But of course when I make a new User() object in my Unit Test it has no ID in it, that is set by the EntityManager (by for me, magic)
I already tried to do something with
$reflectionClass = new ReflectionClass(get_class($user));
$idProperty = $reflectionClass->getProperty('id');
$idProperty->setAccessible(true);
$idProperty->setValue($user, 1);
But this will not help, anyone knows how to fix this error:
1) App\Tests\Service\UserServiceTest::addTest
Expectation failed for method name is equal to 'addUpdate' when invoked 1 time(s)
Parameter 0 for invocation App\Service\DataService::addUpdate(App\Entity\User Object (...)): void does not match expected value.
Failed asserting that two objects are equal.
--- Expected
+++ Actual
## ##
App\Entity\User Object (
- 'id' => 1
+ 'id' => null
'name' => 'test'
'identifier' => null
'email' => 'test#test.nl'
The id from your Entity will only be automatically generated id if you persit and flush.
public function add(Request $request, EntityManagerInterface $em): UserResponse
{
$user = new User();
/** #var $request UserRequest */
$user->setName($request->getName());
$user->setEmail($request->getEmail());
$em->persist($user);
$em->flush();
dd($user->getId);
}

return new Response return empty data

In cotroller i have getVilla metod which return json response.
I get one villa by id from database in $villa variable, and this work fine. When i print_r($villa) i have villa object with all data.
But when i push $villa in response with
json_encode: json_encode(['villa' => $villa]), villa is empty...
villa: {}
/**
* #Route("/ajax/{id}", name="app_post_front_ajax_villa")
* #param $id
* #param EntityManagerInterface $em
* #return Response
*/
public function getVilla($id, EntityManagerInterface $em): Response
{
$repository = $em->getRepository(Villa::class);
$villa = $repository->findOneBy(['id' => $id]);
return new Response(json_encode([
'villa' => $villa,
]));
}
You are returning a json response from a response so you should use JsonResponse instead:
use Symfony\Component\HttpFoundation\JsonResponse;
//...
return new JsonResponse([
'villa' => $villa,
]);
However, your array contain an object $villa, not an array.
So you should either make a new array from your villa object or serialize it.
The easy way would be to make a new array from villa :
public function getVilla($id, EntityManagerInterface $em): Response
{
$repository = $em->getRepository(Villa::class);
$villa = $repository->findOneBy(['id' => $id]);
if($villa){
$villaArray['id'] = $villa->getId();
$villaArray['cityNameOrSomething'] = $villa->getCityName();
//Do the same for other attribute you want to get in your json
}else{
$villaArray = [];
}
return new Response([
'villa' => $villaArray,
]);
}
The other way would be to use the serializer component so that you do not have to make a new array.
Just follow the Symfony Documentation on the usage to find the one you want to use.

How to include fractal transformed objects directly to collection meta without data key

I'm using league/fractal with JsonApiSerializer,
I've got users collection for json output.
Now I want to add some filters data to this json response (like users count for current filters).
I got this:
$resource = new Collection($dataProvider->getData(), new UserTransformer());
//the only way to include some not directly linked data i found is using setMeta():
$resource->setMetaValue('projects', $dataProvider->getProjects());
$resource->setMetaValue('somes', $dataProvider->getTasks());
But! 'projects' & 'somes' collections (yes, they are collection too) also included with 'data' key in it.
So, I've got this structure:
{
'data' => [
{//user1},{//user2},...
],
'meta' => {
'projects' => {
'data' => {...}
},
'somes' => {
'data' => {...}
}
}
}
but I want something like:
{
'data' => [
{//user1},{//user2},...
],
'meta' => {
'projects' => {...}, //there is no 'data' key
'somes' => {...} //there is no 'data' key
}
}
What should I do?
This is kinda hack but works fine without refactor Scope class which hardcoded in fractal's League\Fractal\Manager::createData() and is only way to use your own Scope class realization is to overload this method in Manager's extension.
<?php
use League\Fractal\Serializer\JsonApiSerializer;
/**
* Class EmbedSerializer
*/
class EmbedSerializer extends JsonApiSerializer
{
const RESOURCE_EMBEDDED_KEY = 'embedded';
/**
* Serialize a collection.
*
* #param string $resourceKey
* #param array $data
* #return array
*/
public function collection($resourceKey, array $data)
{
return $resourceKey === self::RESOURCE_EMBEDDED_KEY ? $data : [$resourceKey ?: 'data' => $data];
}
/**
* Serialize an item.
*
* #param string $resourceKey
* #param array $data
* #return array
*/
public function item($resourceKey, array $data)
{
return $resourceKey === self::RESOURCE_EMBEDDED_KEY ? $data : [$resourceKey ?: 'data' => [$data]];
}
}
So, now i could use it like:
/** #var $this->fractal League\Fractal\Manager */
$this->fractal->setSerializer(new EmbedSerializer());
$projectsCollection = $this->fractal->createData(
new Collection($projects, new UserProjectTransformer(), 'embedded')
)->toArray();
$resource = new Collection($users, new UserTransformer());
$resource->setMetaValue('projects', $projectsCollection);
That's all u need. Hope this will be helpful.

Validating symfony collection based on getters instead of properties

I have 2 questions regarding validation. I make a lot of use of property methods (getters) in my entities (nicer code imho). This is one such entity:
class Spec2Events implements ValueAssignable
{
private $values;
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getValues()
{
return $this->values;
}
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getCauseOfDeathValues()
{
$codPms=array();
array_push($codPms,'Cause of death::Natural');
array_push($codPms,'Cause of death::Bycatch');
array_push($codPms,'Cause of death::Ship strike');
array_push($codPms,'Cause of death::Predation');
array_push($codPms,'Cause of death::Other');
array_push($codPms,'Cause of death::Unknown');
return $this->getValues()->filter(
function($entry) use ($codPms) {
return in_array($entry->getPmdSeqno()->getName(), $codPms);
}
);
}
}
$values in this case is a collection of SpecimenValues (which implements EntityValues). ValueAssignables have a collection of EntityValues.
An EntityValuesType class is the form for any class that implements EntityValues. This form has some text or choice childs.
EntityValuesType forms are called like this:
$builder->add('causeOfDeathValues', 'collection', array('type' => new EntityValuesType($this->doctrine),
'options' => array('data_class' => 'AppBundle\Entity\SpecimenValues'),
'allow_delete' => true,
'delete_empty' => true
)); //in order to check if using a class getter as a property works (fails)
$builder->add('values', 'collection', array('type' => new EntityValuesType($this->doctrine),
'options' => array('data_class' => 'AppBundle\Entity\SpecimenValues'),
'allow_delete' => true,
'delete_empty' => true
)); //in order to check if using a class member as a property works (works)
Validation.yml for SpecimenValues looks like this:
AppBundle\Entity\SpecimenValues:
properties:
pmdSeqno:
- NotBlank: ~
- NotNull: ~
s2eScnSeqno:
- NotBlank: ~
- NotNull: ~
description:
- Length:
min: 0
max: 250
value:
- NotBlank: ~
- NotNull: ~
- Length:
min: 1
max: 50
valueFlag:
- Length:
min: 0
max: 50
The Controller looks like this:
public function newAction()
{
$observation = $this->prepareObservation();
$form = $this->createForm(new ObservationsType($this->getDoctrine()), $observation);
return $this->render('AppBundle:Page:add-observations-specimens.html.twig', array(
'form' => $form->createView()
));
}
private function prepareObservation(){
$observation = new Observations();
$event = new EventStates();
$observation->setEseSeqno($event);
$s2e = new Spec2Events();
$event->setSpec2Events($s2e);
$this->instantiateSpecimenValues('Cause of death::Natural', $s2e, false);
$this->instantiateSpecimenValues('Cause of death::Bycatch', $s2e, false);
$this->instantiateSpecimenValues('Cause of death::Ship strike', $s2e, false);
$this->instantiateSpecimenValues('Cause of death::Predation', $s2e, false);
$this->instantiateSpecimenValues('Cause of death::Other', $s2e, false);
$this->instantiateSpecimenValues('Cause of death::Unknown', $s2e, false);
//...
return $observation;
}
private function instantiateSpecimenValues($pmName, &$s2e, $mustBeFlagged)
{
$em = $this->getDoctrine()->getManager();
$pm = $em->getRepository("AppBundle:ParameterMethods")->getParameterMethodByName($pmName);
$sv = new SpecimenValues();
$sv->setPmdSeqno($pm);
$sv->setS2eScnSeqno($s2e);
$sv->setValueFlagRequired($mustBeFlagged);
return $sv;
}
Now, my problem is that empty values are not blocked by the validator (no form error message appears).
If I add a validation constraint programmatically in FormEvents::PRE_SET_DATA, like this:
$options2['constraints'] = array(new \Symfony\Component\Validator\Constraints\NotNull());
it works, but the constraints placed in the .yml file are ignored. Is it possible to combine doing this 'programmatically' AND with validation.yml? In any case I'll write a callback to add in the .yml, so I prefer validation.yml.
Using a form child with name 'values', corresponding to the pure class member variable, works as it should: all required empty fields get a message. All other validation works normally.
What could solve this? I could also use 'values' and use twig to split the collection, but I like using methods as property accessors better.
Thanks!
I've solved this simply by creating the fields both as getters and as properties. The properties themselves are set in the setters. This is necessary otherwise the validator is never called.
So:
/**
* #var \Doctrine\Common\Collections\Collection
* #ORM\OneToMany(targetEntity="AppBundle\Entity\SpecimenValues", mappedBy="s2eScnSeqno")
*/
private $values;
private $causeOfDeathValues;
/**
* #param \Doctrine\Common\Collections\Collection $values
* #return Spec2Events
*/
public function setCauseOfDeathValues(\Doctrine\Common\Collections\Collection $values)
{
$this->causeOfDeathValues=$values;
$this->values= new \Doctrine\Common\Collections\ArrayCollection(
array_merge($this->getValues()->toArray(), $values->toArray())
);
return $this;
}

Zend Framework 2 - Hydrator strategy for Doctrine relationship not working

As mentioned here I'm building a custom hydration strategy to handle my related objects in a select box in a form.
My form looks like this:
$builder = new AnnotationBuilder($entityManager);
$form = $builder->createForm(new MyEntity());
$form->add(new MyFieldSet());
$hydrator = new ClassMethodsHydrator();
$hydrator->addStrategy('my_attribute', new MyHydrationStrategy());
$form->setHydrator($hydrator);
$form->get('my_attribute')->setValueOptions(
$entityManager->getRepository('SecEntity\Entity\SecEntity')->fetchAllAsArray()
);
When I add a new MyEntity via the addAction everything works great.
I wrote fetchAllAsArray() to populate my selectbox. It lives within my SecEntityRepository:
public function fetchAllAsArray() {
$objects = $this->createQueryBuilder('s')
->add('select', 's.id, s.name')
->add('orderBy', 's.name ASC')
->getQuery()
->getResult();
$list = array();
foreach($objects as $obj) {
$list[$obj['id']] = $obj['name'];
}
return $list;
}
But in the edit-case the extract() function doesn't work. I'm not at the point where I see something of hydrate() so I'll leave it out for now.
My hydrator strategy looks like this:
class MyHydrationStrategy extends DefaultStrategy
{
public function extract($value) {
print_r($value);
$result = array();
foreach ($value as $instance) {
print_r($instance);
$result[] = $instance->getId();
}
return $result;
}
public function hydrate($value) {
...
}
The problem is as follows:
Fatal error: Call to a member function getId() on a non-object
The print_r($value) returns loads of stuff beginning with
DoctrineORMModule\Proxy__CG__\SecEntity\Entity\SecEntity Object
following with something about BasicEntityPersister and somewhere in the mess are my referenced entities.
The print_r($instance) prints nothing. It's just empty. Therefore I guess is the error message legit... but why can't I iterate over these objects?
Any ideas?
Edit:
Regarding to #Sam:
My attribute in the entity:
/**
* #ORM\ManyToOne(targetEntity="Path/To/Entity", inversedBy="whatever")
* #ORM\JoinColumn(name="attribute_id", referencedColumnName="id")
* #Form\Attributes({"type":"hidden"})
*
*/
protected $attribute;
My new selectbox:
$form->add(array(
'name' => 'attribute',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'attributes' => array(
'required' => true
),
'options' => array(
'label' => 'MyLabel',
'object_manager' => $entityManager,
'target_class' => 'Path/To/Entity',
'property' => 'name'
)
));
My final hope is that I'm doing something wrong within the controller. Neither my selectbox is preselected nor the value is saved...
...
$obj= $this->getEntityManager()->find('Path/To/Entity', $id);
$builder = new \MyEnity\MyFormBuilder();
$form = $builder->newForm($this->getEntityManager());
$form->setBindOnValidate(false);
$form->bind($obj);
$form->setData($obj->getArrayCopy());
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
$form->bindValues();
$this->getEntityManager()->flush();
return $this->redirect()->toRoute('entity');
}
}
I still haven't come around to write the tutorial for that :S
I don't know if this is working with the annotationbuilder though! As the DoctrineModule\Form\Element\ObjectSelect needs the EntityManager to work. The options for the ObjectSelect are as follows:
$this->add(array(
'name' => 'formElementName',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'attributes' => array(
'required' => true
),
'options' => array(
'label' => 'formElementLabel',
'empty_option' => '--- choose formElementName ---',
'object_manager' => $this->getEntityManager(),
'target_class' => 'Mynamespace\Entity\Entityname',
'property' => 'nameOfEntityPropertyAsSelect'
)
));
In this case i make use of $this->getEntityManager(). I set up this dependency when calling the form from the ServiceManager. Personally i always do this from FactoryClasses. My FormFactory looks like this:
public function createService(ServiceLocatorInterface $serviceLocator)
{
$em = $serviceLocator->get('Doctrine\ORM\EntityManager');
$form = new ErgebnishaushaltProduktForm('ergebnisform', array(
'entity_manager' => $em
));
$classMethodsHydrator = new ClassMethodsHydrator(false);
// Wir fügen zwei Strategien, um benutzerdefinierte Logik während Extrakt auszuführen
$classMethodsHydrator->addStrategy('produktBereich', new Strategy\ProduktbereichStrategy())
->addStrategy('produktGruppe', new Strategy\ProduktgruppeStrategy());
$hydrator = new DoctrineEntity($em, $classMethodsHydrator);
$form->setHydrator($hydrator)
->setObject(new ErgebnishaushaltProdukt())
->setInputFilter(new ErgebnishaushaltProduktFilter())
->setAttribute('method', 'post');
return $form;
}
And this is where all the magic is happening. Magic, that is also relevant to your other Thread here on SO. First, i grab the EntityManager. Then i create my form, and inject the dependency for the EntityManager. I do this using my own Form, you may write and use a Setter-Function to inject the EntityManager.
Next i create a ClassMethodsHydrator and add two HydrationStrategies to it. Personally i need to apply those strategies for each ObjectSelect-Element. You may not have to do this on your side. Try to see if it is working without it first!
After that, i create the DoctrineEntity-Hydrator, inject the EntityManager as well as my custom ClassMethodsHydrator. This way the Strategies will be added easily.
The rest should be quite self-explanatory (despite the german classnames :D)
Why the need for strategies
Imo, this is something missing from the DoctrineEntity currently, but things are still in an early stage. And once DoctrineModule-Issue#106 will be live, things will change again, probably making it more comfortable.
A Strategy looks like this:
<?php
namespace Haushaltportal\Stdlib\Hydrator\Strategy;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
class ProduktbereichStrategy implements StrategyInterface
{
public function extract($value)
{
if (is_numeric($value) || $value === null) {
return $value;
}
return $value->getId();
}
public function hydrate($value)
{
return $value;
}
}
So whenever the $value is not numeric or null, meaning: it should be an Object, we will call the getId() function. Personally i think it's a good idea to give each Element it's own strategy, but if you are sure you won't be needing to change the strategy at a later point, you could create a global Strategy for several elements like DefaultGetIdStrategy or something.
All this is basically the good work of Michael Gallego aka Bakura! In case you drop by the IRC, just hug him once ;)
Edit An additional resource with a look into the future - updated hydrator-docs for a very likely, soon to be included, pull request

Categories