deserialize json to array of entities - symfony

I would like to deserialize a JSON to an object having an entity relation.
incoming JSON data
{
"name": "john",
"books": [
{
"title": "My life"
}
]
}
The result of json deserialization like this
$object = $this->get('serializer')->deserialize($jsonData, 'Author', 'json');
is
Author { #name: 'john' #books: array:1 [ 0 => array:1 [ "title" => "My life" ] ] }
I would like to deserialize to an object like this
Author { #name: 'john' #books: array:1 [ Book { "title" => "My life" } ] }
I understand why deserialization is not able to deserialize Book. With JMSSerialzerBundle, the Type annotation exists to resolve that case.
Is it possible to do it with the Symfony Serializer component or must i use the JMSSerializer for that ?
Thanks for your help ;)
My objects
class Author
{
private $name;
private $books;
/**
* #return mixed
*/
public function getName()
{
return $this->name;
}
/**
* #param mixed $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* #return mixed
*/
public function getBooks()
{
return $this->books;
}
/**
* #param mixed $books
*/
public function setBooks(array $books)
{
$this->books = $books;
}
}
class Book
{
private $title;
private $author;
/**
* #return mixed
*/
public function getTitle()
{
return $this->title;
}
/**
* #param mixed $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* #return mixed
*/
public function getAuthor()
{
return $this->author;
}
/**
* #param mixed $author
*/
public function setAuthor(Author $author)
{
$this->author = $author;
}
}

Guilhem is right, the default Symfony ObjectNormalizer isn't able to normalize properties of non scalar types for now.
However, this feature is being added in Symfony 3.1: https://github.com/symfony/symfony/pull/17660
In the meantime, you can copy/paste the ObjectNormalizer version provided in the PR linked above in your project.
You can also take a look at a similar implementation available in API Platform:
https://github.com/api-platform/core/blob/master/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyMetadataFactory.php#L48-L51
https://github.com/api-platform/core/blob/master/src/JsonLd/Serializer/ItemNormalizer.php#L155-L178

The symfony serializer can't denormalize complex properties.
I think that the only way to do that is to manage your object denormalization by yourself:
use Symfony\Component\Serializer\Normalizer\DenormalizableInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class Author implements DenormalizableInterface {
public function denormalize(DenormalizerInterface $denormalizer, $data, $format = null, array $context = array()) {
if (isset($data['name'])) {
$this->setName($data['name']);
}
if (isset($data['books']) && is_array($data['books'])) {
$books = array();
foreach ($data['books'] as $book) {
$books[] = $denormalizer->denormalize($book, Book::class, $format, $context);
}
$this->setBooks($books);
}
}
// ...
}
You can also create a custom normalizer but this is more complicated (you can take a look at this article which explains more or less how to do that).
I hope this will help you :-)

Related

How can i limit the number of nested entities in API Platform?

Having two related entities, let's say Author and Book, I can limit (or paginate) the results of Authors but not the number of results of its related entity Books which always shows the whole collection.
The issue is that Authors may have hundreds of Books making the resulting JSON huge and heavy to parse so I'm trying to get, for example, only the last 5 books.
I'm sure I'm missing something since I think this is probably a common scenario but I can't find anything on the docs nor here in StackOverflow.
I'm starting with Api Platform, any hint would be appreciated!
I finally solved it creating a normalizer for the entity but I still think that it has to be a simpler solution.
Here's what I had to do, following the Authors / Books example:
Add a setter to the Author entity to override the Author's Book collection:
// src/Entity/Author.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
// ...
/**
* #ApiResource
* #ORM\Entity(repositoryClass="App\Repository\AuthorRepository")
*/
class Author
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Book", mappedBy="author", orphanRemoval=true)
*/
private $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
// Getters and setters
//...
public function setBooks($books): self
{
$this->books = $books;
return $this;
}
}
Create a normalizer for the Author's entity:
// App/Serializer/Normalizer/AuthorNormalizer.php
<?php
namespace App\Serializer\Normalizer;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
class AuthorNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
use SerializerAwareTrait;
private $normalizer;
public function __construct(
NormalizerInterface $normalizer,
IriConverterInterface $iriConverter
) {
if (!$normalizer instanceof DenormalizerInterface) {
throw new \InvalidArgumentException('The normalizer must implement the DenormalizerInterface');
}
if (!$normalizer instanceof AbstractItemNormalizer) {
throw new \InvalidArgumentException('The normalizer must be an instance of AbstractItemNormalizer');
}
$handler = function ($entity) use ($iriConverter) {
return $iriConverter->getIriFromItem($entity);
};
$normalizer->setMaxDepthHandler($handler);
$normalizer->setCircularReferenceHandler($handler);
$this->normalizer = $normalizer;
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->normalizer->denormalize($data, $class, $format, $context);
}
public function supportsDenormalization($data, $type, $format = null)
{
return $this->normalizer->supportsDenormalization($data, $type, $format);
}
public function normalize($object, $format = null, array $context = [])
{
// Number of desired Books to list
$limit = 2;
$newBooksCollection = new ArrayCollection();
$books = $object->getBooks();
$booksCount = count($books);
if ($booksCount > $limit) {
// Reverse iterate the original Book collection as I just want the last ones
for ($i = $booksCount; $i > $booksCount - $limit; $i--) {
$newBooksCollection->add($books->get($i - 1));
}
}
// Setter previously added to the Author entity to override its related Books
$object->setBooks($newBooksCollection);
$data = $this->normalizer->normalize($object, $format, $context);
return $data;
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof \App\Entity\Author;
}
}
And finally register the normalizer as a service manually (using autowire led me to circular reference issues):
services:
App\Serializer\Normalizer\AuthorNormalizer:
autowire: false
autoconfigure: true
arguments:
$normalizer: '#api_platform.jsonld.normalizer.item'
$iriConverter: '#ApiPlatform\Core\Api\IriConverterInterface'

File upload Sonata Admin from documentation does not work

Im trying to upload a file using SonataAdminBundle.
I don't know what am I doing wrong as I followed instructions from official documentation https://sonata-project.org/bundles/admin/3-x/doc/cookbook/recipe_file_uploads.html
The upload() function is not triggering, and even when I invoke the method in DocumentAdmin it does not put the files in the directory I specified.
It seems like the yaml file is not even read, but how am I supposed to configure it ?
prePersist() and preUpdate() are triggered even without it.
Code:
final class DocumentAdmin extends AbstractAdmin
{
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('title', null, [
'label' => 'Name'
])
->add('documentCategory', null, [
'label' => 'Typ'
])
->add('priority', null, [
'label' => 'Order'
])
;
}
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('file', FileType::class, [
'required' => true,
])
->add('title', null, [
'label' => 'Name'
])
->add('priority', null, [
'label' => 'Priority'
])
->add('documentCategory', null, [
'label' => 'Typ'
])
;
}
public function prePersist($document)
{
$this->manageFileUpload($document);
}
public function preUpdate($document)
{
$this->manageFileUpload($document);
}
private function manageFileUpload($document)
{
if ($document->getFile()) {
$document->refreshUpdated();
}
}
public function toString($object)
{
return $object instanceof Document
? $object->getTitle()
: 'File'; // shown in the breadcrumb on the create view
}
}
Document Entity
class Document
{
const SERVER_PATH_TO_DOCUMENTS_FOLDER = '%kernel.project_dir%/public/uploads';
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* Unmapped property to handle file uploads
*/
private $file;
/**
* #param UploadedFile $file
*/
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
}
/**
* #return UploadedFile
*/
public function getFile()
{
return $this->file;
}
/**
* Manages the copying of the file to the relevant place on the server
*/
public function upload()
{
// the file property can be empty if the field is not required
if (null === $this->getFile()) {
return;
}
// we use the original file name here but you should
// sanitize it at least to avoid any security issues
// move takes the target directory and target filename as params
$file = $this->getFile();
$directory = self::SERVER_PATH_TO_DOCUMENTS_FOLDER;
$originaName = $this->getFile()->getClientOriginalName();
// dump($file);
// dump($directory);
// dump($originaName);
// die();
$file->move($directory, $originaName);
// set the path property to the filename where you've saved the file
// clean up the file property as you won't need it anymore
$this->setFile(null);
}
/**
* Lifecycle callback to upload the file to the server.
*/
public function lifecycleFileUpload()
{
$this->upload();
}
/**
* Updates the hash value to force the preUpdate and postUpdate events to fire.
*/
public function refreshUpdated()
{
$this->setDateOfUpload(new \DateTime());
}
/**
* #ORM\Column
* #Gedmo\UploadableFileName
*/
private $title;
/**
* #Gedmo\Timestampable(on="create")
* #ORM\Column(type="datetime")
*/
private $dateOfUpload;
/**
* #ORM\Column(type="smallint")
*/
private $priority;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\DocumentCategory", inversedBy="documents")
* #ORM\JoinColumn(nullable=false)
*/
private $documentCategory;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
/**
* #return \DateTime
*/
public function getDateOfUpload()
{
return $this->dateOfUpload;
}
public function setDateOfUpload(\DateTimeInterface $dateOfUpload): self
{
$this->dateOfUpload = new \DateTime();
return $this;
}
public function getPriority(): ?int
{
return $this->priority;
}
public function setPriority(int $priority): self
{
$this->priority = $priority;
return $this;
}
public function getDocumentCategory(): ?DocumentCategory
{
return $this->documentCategory;
}
public function setDocumentCategory(?DocumentCategory $documentCategory): self
{
$this->documentCategory = $documentCategory;
return $this;
}
// public function myCallbackMethod(array $info)
// {
// }
public function __toString()
{
return $this->title;
}
}
EDIT: I changed the file directory path to:
const SERVER_PATH_TO_DOCUMENTS_FOLDER = 'uploads/documents';
And also invoked the lifecycleFileUpload() method in manageFileUpload() method
and now the files are moved to the directory.
Is it the right way to upload ?

JMSSerializerBundle deserialization skip groups exclusion on id property using DoctrineObjectConstructor

I'm using jms/serializer-bundle 2.4.3 on a symfony 4.2 and a I noticed an annoying problem in my application :
when I post an entity, the DoctrineObjectConstructor uses id in content to retrieve another entity and thus patch it while it is excluded by my security groups
see rather entity
class Entity
{
/**
* #var int
*
* #ORM\Column(name="id", type="int")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #Serializer\Groups({"GetEntity"})
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string")
* #Serializer\Groups({"GetEntity", "PostEntity"})
*/
private $name;
}
controller
/**
* #Route("/entity", name="post_entity", methods={"POST"})
*/
public function postEntity(Request $request, EntityManagerInterface $entityManager, SerializerInterface $serializer): JsonResponse
{
$deserializationContext = DeserializationContext::create();
$deserializationContext->setGroups(['PostEntity']);
$entity = $serializer->deserialize($request->getContent(), Entity::class, 'json', $deserializationContext);
$entityManager->persist($entity);
$entityManager->flush();
return $this->json($entity, Response::HTTP_OK, [], ['groups' => ['GetEntity']]);
}
I have some JMS configurations changes in services
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: true
jms_serializer.unserialize_object_constructor:
class: App\Serializer\ObjectConstructor
If anyone can explain to me how to ignore the id in this case I'm open to any suggestions.
Regards and thanks for any help
To resolve, just add override in your services.yaml
jms_serializer.doctrine_object_constructor:
class: App\Serializer\DoctrineObjectConstructor
arguments:
- '#doctrine'
- '#jms_serializer.unserialize_object_constructor'
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
and add a local DoctrineObjectConstructor updated to ignore entities without current deserialization group on id property
class DoctrineObjectConstructor implements ObjectConstructorInterface
{
const ON_MISSING_NULL = 'null';
const ON_MISSING_EXCEPTION = 'exception';
const ON_MISSING_FALLBACK = 'fallback';
private $fallbackStrategy;
private $managerRegistry;
private $fallbackConstructor;
/**
* Constructor.
*
* #param ManagerRegistry $managerRegistry Manager registry
* #param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
* #param string $fallbackStrategy
*/
public function __construct(ManagerRegistry $managerRegistry, ObjectConstructorInterface $fallbackConstructor, $fallbackStrategy = self::ON_MISSING_NULL)
{
$this->managerRegistry = $managerRegistry;
$this->fallbackConstructor = $fallbackConstructor;
$this->fallbackStrategy = $fallbackStrategy;
}
/**
* {#inheritdoc}
*/
public function construct(VisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context)
{
// Locate possible ObjectManager
$objectManager = $this->managerRegistry->getManagerForClass($metadata->name);
if (!$objectManager) {
// No ObjectManager found, proceed with normal deserialization
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
// Locate possible ClassMetadata
$classMetadataFactory = $objectManager->getMetadataFactory();
if ($classMetadataFactory->isTransient($metadata->name)) {
// No ClassMetadata found, proceed with normal deserialization
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
// Managed entity, check for proxy load
if (!\is_array($data)) {
// Single identifier, load proxy
return $objectManager->getReference($metadata->name, $data);
}
// Fallback to default constructor if missing identifier(s)
$classMetadata = $objectManager->getClassMetadata($metadata->name);
$identifierList = [];
foreach ($classMetadata->getIdentifierFieldNames() as $name) {
$propertyGroups = [];
if ($visitor instanceof AbstractVisitor) {
/** #var PropertyNamingStrategyInterface $namingStrategy */
$namingStrategy = $visitor->getNamingStrategy();
$dataName = $namingStrategy->translateName($metadata->propertyMetadata[$name]);
$propertyGroups = $metadata->propertyMetadata[$name]->groups;
} else {
$dataName = $name;
}
if (!array_key_exists($dataName, $data) || true === empty(array_intersect($context->getAttribute('groups'), $propertyGroups))) {
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
$identifierList[$name] = $data[$dataName];
}
// Entity update, load it from database
$object = $objectManager->find($metadata->name, $identifierList);
if (null === $object) {
switch ($this->fallbackStrategy) {
case self::ON_MISSING_NULL:
return null;
case self::ON_MISSING_EXCEPTION:
throw new ObjectConstructionException(sprintf('Entity %s can not be found', $metadata->name));
case self::ON_MISSING_FALLBACK:
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
default:
throw new InvalidArgumentException('The provided fallback strategy for the object constructor is not valid');
}
}
$objectManager->initializeObject($object);
return $object;
}
}

Symfony2 form and Doctrine2 - creat foreign key in assigned entities fails

i'm trying to add my data into my database , i was trying to not use a formbuilder, inside that i put all my form into the controller,and my entity contains a foreign key but i got an this error :
Neither the property "id_classe" nor one of the methods "getIdClasse()", "idClasse()", "isIdClasse()", "hasIdClasse()", "__get()" exist and have public access in class "MyApp\SchoolBundle\Entity\Etudiant".
here is my function in the controller :
public function AjoutAction(Request $request)
{ $classe=new Etudiant();
$formBuilder = $this->get('form.factory')->createBuilder('form', $classe);
$formBuilder
->add('prenom', 'text')
->add('nom', 'text')
->add('Cin', 'integer')
->add('id_classe', 'integer')
->add('save', 'submit')
;
$form = $formBuilder->getForm();
if ($form->handleRequest($request)->isValid()) {
$objToPersist = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($objToPersist);
$em->flush();
}
return $this->render('MyAppSchoolBundle:Etudiant:ajout.html.twig',array(
'form' => $form->createView(),
));
}
and here is my Entity
namespace MyApp\SchoolBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* #ORM\Entity
*/
class Etudiant {
/**
* #ORM\id
*#ORM\GeneratedValue
*#ORM\Column(type="integer",name="ID_Etudiant")
*/
private $Id;
/**
*#ORM\Column{type="string",length=255}
*/
private $prenom;
/**
*#ORM\Column{type="string",length=255}
*/
private $nom;
/**
*#Assert\NotBlank
*#ORM\Column(type="integer", unique=true)
*/
private $cin; //unique ne fonctionne pas qu'avec les assert
/**
* #ORM\ManyToOne(targetEntity="Classes",cascade={"ALL"})
*/
private $id_classe;
function getId() {
return $this->Id;
}
function getPrenom() {
return $this->prenom;
}
function getNom() {
return $this->nom;
}
function setId($Id) {
$this->Id = $Id;
}
function setPrenom($prenom) {
$this->prenom = $prenom;
}
function setNom($nom) {
$this->nom = $nom;
}
function getCin() {
return $this->cin;
}
function setCin($cin) {
$this->cin = $cin;
}
public function getId_classe() {
return $this->id_classe;
}
function setId_classe($id_classe) {
$this->id_classe = $id_classe;
}
}
In your form you have:
->add('id_classe', 'integer')
Add a setter in your entity
public function setIdClasse($idClasse) {
$this->id_classe = $idClasse;
}
Edit
Also, as a suggestion:
1 Always add visibility to your functions (public function blabla() or private function blabla())
2 Use camel case is preferred (so your properties are $nomClasse, $idClasse, $id, etc..)
3 Not compulsory, but a good idea to return the object in your setter
4 You're not very consistent in your notations (see your form builder->add('nom', 'text') ->add('Cin', 'integer'))
Getters and Setter would normally look like this:
public function getNomClasse()
{
return $this->nomClasse;
}
public function setNomClasse($nomClasse)
{
$this->nomClasse = $nomClasse;
return $this;
}

Problems using a sonata_type_collection for a One-Many-One relation

I have a fairly common use case which I am trying implement but am running into some issues with the Symfony Sonata Admin Bundle (ORM). My model has a relationship between a Facility and a Sport which is based on three entity classes: Sport, Facility and SportsFacility. I followed the example http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/form_field_definition.html#advanced-usage-one-to-many and defined in the following classes (relevant parts only).
class Sport {
/**
* Bidirectional - Many facilies are related to one sport
*
* #ORM\OneToMany(targetEntity="SportsFacility", mappedBy="sport")
*/
protected $facilities;
public function getFacilities() {
return $this->facilities;
}
public function setFacilities($facilities) {
$this->facilities = $facilities;
return $this;
}
}
class Facility {
/**
* Bidirectional - Many sports are related to one facility
*
* #ORM\OneToMany(targetEntity="SportsFacility", mappedBy="facility")
*/
protected $sports;
public function getSports() {
return $this->sports;
}
public function setSports($sportsFacilities) {
$this->sports = $sportsFacilities;
return $this;
}
public function addSports($sportsFacilities) {
$this->sports = $sportsFacilities;
return $this;
}
}
class SportsFacility {
/**
* #var integer $sportsFacilityId
*
* #ORM\Column(name="sportsFacilityId", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $sportsFacilityId;
/**
* Bidirectional - Many Sports are related to one Facility (OWNING SIDE)
*
* #ORM\ManyToOne(targetEntity="Sport", inversedBy="facilities"))
* #ORM\JoinColumn(name="sportId", referencedColumnName="sportId"))
*/
protected $sport;
/**
* Bidirectional - Many Facilities are related to one Sport (OWNING SIDE)
*
* #ORM\ManyToOne(targetEntity="Facility", inversedBy="sports"))
* #ORM\JoinColumn(name="facilityId", referencedColumnName="facilityId"))
*/
protected $facility;
public function getSportsFacilityId() {
return $this->sportsFacilityId;
}
public function setSportsFacilityId($sportsFacilityId) {
$this->sportsFacilityId = $sportsFacilityId;
return $this;
}
public function getSport() {
return $this->sport;
}
public function setSport($sport) {
$this->sport = $sport;
return $this;
}
public function getFacility() {
return $this->facility;
}
public function setFacility($facility) {
$this->facility = $facility;
return $this;
}
}
In my FacilityAdmin class I have:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('name')
->with('Sports')
->add('sports', 'sonata_type_collection',
array('by_reference' => false),
array(
'edit' => 'inline',
'inline' => 'table',
))
->end();
}
When I try to add a new relation, I get the following error:
Expected argument of type "array or \Traversable", "Clarity\CoachTimeBundle\Entity\SportsFacility" given in "vendor/sonata-project/admin-bundle/Sonata/AdminBundle/Form/EventListener/ResizeFormListener.php at line 88"
Finally found where we have the problem; in your class Facility you added the missing method for sonata, but it shouldn't do that you think it should do (yup yup) :
class Facility {
...
/**
* Alias for sonata
*/
public function addSports($sportFacility) {
return $this->addSport($sportFacility);
}
}
I presume addSport() is the default doctrine generated method to add new instance of SportsFacilityto collection.
This is due to how sonata generate the method to add a new entity that is different how doctrine do :
//file: Sonata/AdminBundle/Admin/AdminHelper.php
public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
{
...
$method = sprintf('add%s', $this->camelize($mapping['fieldName']));
...
}
Someone got the same problem but the documentation didn't evolve

Resources