I discover the serializer and deserializer.
I have an index.json file and I'm trying to deserializer.
The json_decode works (I have many tables as a result) however with the deserializer I have everything at null and I don't understand why. (I begin)
index.json:
[
{
"label": "VILLA GALLO-ROMAINE DU GROSSWALD",
"lastUpdateDatatourisme": "2022-09-24T04:00:46.397Z",
"file": "0/00/10-000283ba-f94b-3bce-8f57-e5c10bec6cd4.json"
},
{
"label": "ÉGLISE PAROISSIALE DE LA NATIVITÉ",
"lastUpdateDatatourisme": "2022-08-07T04:02:23.588Z",
"file": "0/00/10-0016e4ef-68b7-3e79-87c4-5a277e1a43ac.json"
},
{
"label": "CHEZ LES P'TITS PAYSANS",
"lastUpdateDatatourisme": "2022-11-20T06:05:43.206Z",
"file": "0/00/10-001dc202-cf27-355f-8577-6ef3105bddec.json"
},(...)
my class:
<?php
namespace App\Entity;
use App\Repository\IndexDataTourismeRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: IndexDataTourismeRepository::class)]
class IndexDataTourisme
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $label = null;
#[ORM\Column(length: 255)]
private ?string $lastUpdateDatatourisme = null;
#[ORM\Column(length: 255)]
private ?string $file = null;
my controller:
class HomeController extends AbstractController
{
#[Route('/home', name: 'app_home')]
public function index(): Response
{
$encoders = [new XmlEncoder(), new JsonEncoder()];
$normalizers = [new ObjectNormalizer()];
$serializer = new Serializer($normalizers, $encoders);
$person = $serializer->deserialize($data, IndexDataTourisme::class, 'json', [AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => false]);
dd($person);
return $this->render('home/index.html.twig', [
'controller_name' => 'HomeController',
]);
}
in my opinion it is a visible error for you, but I pass by without understanding...
and also, if ever a new field is added to my index.json, I want my application to continue to work with the dezerializer.
I use symfony 6.2
thank you
Related
I have the following entity in my api-platform:
#[ApiResource(
operations: [
new Get(),
new GetCollection(),
],
normalizationContext: ['groups' => ['read']],
)]
#[ApiFilter(
filterClass: SearchFilter::class,
properties: [
"id" => SearchFilterInterface::STRATEGY_EXACT,
'hash' => SearchFilterInterface::STRATEGY_EXACT,
'used' => SearchFilterInterface::STRATEGY_EXACT,
]
)]
class ResetPasswordHashes
{
#[ORM\Id, ORM\Column(name: "id"), ORM\GeneratedValue]
#[Groups(['read'])]
public int $id;
#[ORM\Column(name: "hash", length: 255)]
#[Groups(['read'])]
public string $hash;
#[ORM\Column(name: 'used')]
#[Groups(['read'])]
public bool $used;
#[ORM\OneToOne(targetEntity: Contact::class)]
#[ORM\JoinColumn(name: 'contact_id', referencedColumnName: 'contact_id')]
#[Groups(['read'])]
public ?Contact $contact;
#[ORM\Column(name: "created_at")]
#[Groups(['read'])]
public ?\DateTime $createdAt;
This is just an excerpt of the relevant lines. Idealy I only want to return the information when the the $used is false. Is there a way to set up the entity where I protect the $contact information if $used == false?
Here is an example to protect the $contact information if $used is false:
#[ApiResource(
operations: [
new Get(),
new GetCollection(),
],
normalizationContext: ['groups' => ['read']],
)]
class ResetPasswordHashes
{
//...
public bool $used;
#[Groups(['read'])]
public ?Contact $contact;
//...
#[Serializer\Groups({"read"})]
public function isUsed(): bool
{
return $this->used;
}
}
The isUsed method is decorated with the #Serializer\Groups annotation, which specifies that it should be included in the "read" group.
Then in your controller you can use the Symfony's serializer component to handle the serialization and apply the isUsed method as a condition for the inclusion of the $contact property:
/**
* #Route("/reset-password-hashes/{id}", name="reset_password_hashes_get", methods={"GET"})
*/
public function get(ResetPasswordHashes $resetPasswordHashes, SerializerInterface $serializer)
{
$data = $serializer->normalize($resetPasswordHashes, null, [
'groups' => ['read'],
'used' => $resetPasswordHashes->isUsed() ? ['contact'] : [],
]);
return new JsonResponse($data);
}
This way the $contact property will only be included in the serialization if $used is false.
I have two related doctrine entities:
Content
#[ApiResource(operations: [new Get(), new Patch(), new Delete(), new Put(), new Post(uriTemplate: '/contents/add_text', controller: ContentTextPersist::class, read: false, openapiContext: ['description' => 'This endpoint provides a way to add content text and variants to a content object without incurring circular reference issue as a result of the nested nature of the Content -> ContentText and ContentTextVariants.', 'summary' => 'Persist new content text item and it\'s translations.']), new Post(), new GetCollection()], order: ['createdAt' => 'DESC'], normalizationContext: ['groups' => ['content:read']], denormalizationContext: ['groups' => ['content:write']], filters: ['translation.groups'])]
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class Content extends AbstractTranslatable
{
....
/**
* #var \Doctrine\Common\Collections\Collection<int, \App\Entity\ContentText>|\App\Entity\ContentText[]
*/
#[ORM\OneToMany(targetEntity: ContentText::class, mappedBy: 'content', orphanRemoval: true, cascade: ['persist'])]
#[Groups(['content:read', 'content:write'])]
private iterable $text;
...
public function __construct()
{
parent::__construct();
$this->text = new ArrayCollection();
}
...
/**
* #return Collection|ContentText[]
*/
public function getText() : Collection
{
return $this->text;
}
public function addText(ContentText $text) : self
{
if (!$this->text->contains($text)) {
$this->text[] = $text;
$text->setContent($this);
}
return $this;
}
public function removeText(ContentText $text) : self
{
if ($this->text->removeElement($text)) {
// set the owning side to null (unless already changed)
if ($text->getContent() === $this) {
$text->setContent(null);
}
}
return $this;
}
}
ContentText
#[ApiResource(operations: [new Patch(), new Delete(), new Get(), new GetCollection()], order: ['createdAt' => 'DESC'], paginationPartial: true, normalizationContext: ['groups' => ['contentText:read']], denormalizationContext: ['groups' => ['contentText:write']], filters: ['translation.groups'])]
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class ContentText
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[Groups(['contentText:read', 'content:read'])]
private ?int $id = null;
public function getId() : ?int
{
return $this->id;
}
}
Would anyone know why if I remove #[ApiResource] attribute from ContentText entity then I'm able to get the collection of ContentText related to Content otherwise I get the error below:
"Unable to generate an IRI for the item of type \"App\\Entity\\ContentText\""
I'm learning Symfony by working on a personal project. I'm using Symfony 6.1.
I have a page containing two forms :
the "main" form ($bookForm) deals the Book entity and persist it to the database
the second one ($isbnForm) takes a string and search the Google Books API.
When submitting the second form ($isbnForm), the page is reloaded and $bookForm is preloaded with data.
Here is the code of the BookController :
#[Route('/create', name: 'create', methods: ['GET', 'POST'])]
public function new(
Request $request,
BookshelfRepository $bookshelfRepository,
AuthorRepository $authorRepository,
PublisherRepository $publisherRepository,
BookRepository $bookRepository
): Response {
$this->denyAccessUnlessGranted('edit', $this->getUser());
$book = new Book();
// Retrieving the Bookshelf passed along the request
// and associating it to the Book
if ($request->query->get('bksid')) {
$bookshelf = $bookshelfRepository->findOneBy(['ulid' => $request->query->get('bksid')]);
$book->setBookshelf($bookshelf);
}
// Dealing with the ISBN form
$isbnForm = $this->createForm(IsbnType::class);
$isbnForm->handleRequest($request);
if ($isbnForm->isSubmitted() && $isbnForm->isValid()) {
$isbnTools = new IsbnTools();
$isbn = $isbnTools->format($isbnForm->getData()['isbn']);
$book->setIsbn($isbn);
// Getting book's details from the Google Books API using the ISBN
$gbapi = new GoogleBooksApiUtils();
$details = $gbapi->gettingVolumeInfoByIsbn($isbn);
$book->setTitle($details->getTitle());
$book->setSubtitle($details->getSubtitle());
$book->setDescription($details->getDescription());
$book->setPages($details->getPageCount());
$book->setPublicationDate(substr($details->getPublishedDate(), 0, 4));
if ($details->getPublisher()) {
$publisher = $publisherRepository->findOneBy(['name' => $details->getPublisher()]);
if (!$publisher) {
$publisher = new Publisher();
$publisher->setName($details->getPublisher());
$publisherRepository->save($publisher, true);
}
$book->setPublisher($publisher);
}
foreach ($details->getAuthors() ?? [] as $dga) {
$author = $authorRepository->findOneBy(['name' => $dga]);
if (!$author) {
$author = new Author();
$author->setName($dga);
$authorRepository->save($author, true);
}
$book->addAuthor($author);
}
}
// Dealing with the main form, dealing with the Book entity
$bookForm = $this->createForm(BookType::class, $book);
$bookForm->handleRequest($request);
if ($bookForm->isSubmitted() && $bookForm->isValid()) {
$bookRepository->save($book, true);
return $this->redirectToRoute('bks_book_view', [
'ulid' => $book->getUlid(),
], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('book/create.html.twig', [
'isbn_form' => $isbnForm,
'form' => $bookForm,
'book' => $book
]);
}
It works nicely until I tried to submit the thumbnail get from the Google Books API.
I've installed the VichUploaderBundle, set up a mapping, modified my Book entity accordingly to the documentation.
vich_uploader:
db_driver: orm
metadata:
type: attribute
mappings:
books:
uri_prefix: /uploads/books
upload_destination: '%kernel.project_dir%/public/uploads/books'
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
inject_on_load: false
delete_on_update: true
delete_on_remove: true
<?php
namespace App\Entity;
use App\Repository\BookRepository;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Uid\Ulid;
use Vich\UploaderBundle\Mapping\Annotation\Uploadable;
use Vich\UploaderBundle\Mapping\Annotation\UploadableField;
#[ORM\Entity(repositoryClass: BookRepository::class)]
#[ORM\HasLifecycleCallbacks]
#[Uploadable]
class Book
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $subtitle = null;
#[ORM\ManyToMany(targetEntity: Author::class, inversedBy: 'books')]
private Collection $authors;
#[ORM\ManyToOne(inversedBy: 'books')]
#[ORM\JoinColumn(nullable: false)]
private ?Publisher $publisher = null;
#[ORM\Column(length: 4, nullable: true)]
private ?string $publication_date = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 13, nullable: true)]
private ?string $isbn = null;
#[ORM\Column(nullable: true)]
private ?int $pages = null;
#[ORM\Column(type: 'ulid')]
private ?Ulid $ulid = null;
#[ORM\ManyToOne(inversedBy: 'books')]
#[ORM\JoinColumn(nullable: false)]
private ?Bookshelf $bookshelf = null;
#[UploadableField(mapping: 'books', fileNameProperty: 'imageName', size: 'imageSize')]
private ?File $imageFile = null;
#[ORM\Column(type: 'string', nullable: true)]
private ?string $imageName = null;
#[ORM\Column(type: 'integer', nullable: true)]
private ?int $imageSize = null;
#[ORM\Column(type: 'datetime')]
private ?\DateTimeInterface $updatedAt = null;
public function __construct()
{
$this->authors = new ArrayCollection();
$this->updatedAt = new DateTime();
}
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;
}
public function getSubtitle(): ?string
{
return $this->subtitle;
}
public function setSubtitle(?string $subtitle): self
{
$this->subtitle = $subtitle;
return $this;
}
/**
* #return Collection<int, Author>
*/
public function getAuthors(): Collection
{
return $this->authors;
}
public function addAuthor(Author $author): self
{
if (!$this->authors->contains($author)) {
$this->authors->add($author);
}
return $this;
}
public function removeAuthor(Author $author): self
{
$this->authors->removeElement($author);
return $this;
}
public function getPublisher(): ?Publisher
{
return $this->publisher;
}
public function setPublisher(?Publisher $publisher): self
{
$this->publisher = $publisher;
return $this;
}
public function getPublicationDate(): ?string
{
return $this->publication_date;
}
public function setPublicationDate(?string $publication_date): self
{
$this->publication_date = $publication_date;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
public function getIsbn(): ?string
{
return $this->isbn;
}
public function setIsbn(?string $isbn): self
{
$this->isbn = $isbn;
return $this;
}
public function getPages(): ?int
{
return $this->pages;
}
public function setPages(?int $pages): self
{
$this->pages = $pages;
return $this;
}
public function getUlid(): ?Ulid
{
return $this->ulid;
}
#[ORM\PrePersist]
public function setUlid(): void
{
$this->ulid = new Ulid();
}
public function getBookshelf(): ?Bookshelf
{
return $this->bookshelf;
}
public function setBookshelf(?Bookshelf $bookshelf): self
{
$this->bookshelf = $bookshelf;
return $this;
}
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile|null $imageFile
*/
public function setImageFile(?File $imageFile = null): void
{
$this->imageFile = $imageFile;
if (null !== $imageFile) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->updatedAt = new \DateTimeImmutable();
}
}
public function getImageFile(): ?File
{
return $this->imageFile;
}
public function setImageName(?string $imageName): void
{
$this->imageName = $imageName;
}
public function getImageName(): ?string
{
return $this->imageName;
}
public function setImageSize(?int $imageSize): void
{
$this->imageSize = $imageSize;
}
public function getImageSize(): ?int
{
return $this->imageSize;
}
}
Then I modify my BookController in order to add the imageFile to the Book entity :
#[Route('/create', name: 'create', methods: ['GET', 'POST'])]
public function new(
Request $request,
BookshelfRepository $bookshelfRepository,
AuthorRepository $authorRepository,
PublisherRepository $publisherRepository,
BookRepository $bookRepository
): Response {
$this->denyAccessUnlessGranted('edit', $this->getUser());
$book = new Book();
// Retrieving the Bookshelf passed along the request
// and associating it to the Book
if ($request->query->get('bksid')) {
$bookshelf = $bookshelfRepository->findOneBy(['ulid' => $request->query->get('bksid')]);
$book->setBookshelf($bookshelf);
}
// Dealing with the ISBN form
$isbnForm = $this->createForm(IsbnType::class);
$isbnForm->handleRequest($request);
if ($isbnForm->isSubmitted() && $isbnForm->isValid()) {
$isbnTools = new IsbnTools();
$isbn = $isbnTools->format($isbnForm->getData()['isbn']);
$book->setIsbn($isbn);
// Getting book's details from the Google Books API using the ISBN
$gbapi = new GoogleBooksApiUtils();
$details = $gbapi->gettingVolumeInfoByIsbn($isbn);
$book->setTitle($details->getTitle());
$book->setSubtitle($details->getSubtitle());
$book->setDescription($details->getDescription());
$book->setPages($details->getPageCount());
$book->setPublicationDate(substr($details->getPublishedDate(), 0, 4));
if ($details->getPublisher()) {
$publisher = $publisherRepository->findOneBy(['name' => $details->getPublisher()]);
if (!$publisher) {
$publisher = new Publisher();
$publisher->setName($details->getPublisher());
$publisherRepository->save($publisher, true);
}
$book->setPublisher($publisher);
}
foreach ($details->getAuthors() ?? [] as $dga) {
$author = $authorRepository->findOneBy(['name' => $dga]);
if (!$author) {
$author = new Author();
$author->setName($dga);
$authorRepository->save($author, true);
}
$book->addAuthor($author);
}
$thumb = $details->getImageLinks()->getThumbnail() ?? null;
if ($thumb) {
$path = 'uploads/books';
$filename = 'gbapi_' . $book->getIsbn() . '.jpg';
file_put_contents("$path/$filename", file_get_contents($thumb));
$file = new UploadedFile("$path/$filename", $filename, 'image/jpeg', null, false);
$book->setImageFile($file);
$book->setImageName($file->getFilename());
$book->setImageSize($file->getSize());
}
}
// Dealing with the main form, dealing with the Book entity
$bookForm = $this->createForm(BookType::class, $book);
$bookForm->handleRequest($request);
if ($bookForm->isSubmitted() && $bookForm->isValid()) {
$bookRepository->save($book, true);
return $this->redirectToRoute('bks_book_view', [
'ulid' => $book->getUlid(),
], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('book/create.html.twig', [
'isbn_form' => $isbnForm,
'form' => $bookForm,
'book' => $book
]);
}
When I submit the $isbnForm, the data are well passed to the Book entity, and the image is displayed inside the $bookForm. But when I submit it, the imageFile (imageSize and imageName fields) are not persisted to the database.
I've tried to add the UploadedFile to the $request (something like that : $request->files->set('book', ['imageFile' => ['file' => $file]])) but without success...
I have been trying for two days to solve this issue and I'm not even sure I've identified clearly the issue...
Thanks in advance for any lead :-)
The following error occurs during object deserialization:
Symfony\Component\Serializer\Exception\NotNormalizableValueException
The type of the "products" attribute for class "shop\manage\flexbe\objects\Lead" must be one of "shop\manage\flexbe\objects\Product[]" ("array" given).
Have JSON-object:
{
"id": "9757241",
"time": "1567105530",
// other params
"products": [
{
"title": "Product name",
"count": 1
}
]
}
In the Lead class that describes the object related methods to "products":
private $products = [];
/**
* #return Product[]
*/
public function getProducts()
{
return $this->products;
}
/**
* #param Product $product
*/
public function addProduct(Product $product): void
{
$this->products[] = $product;
}
Deserialization Code:
$normalizer = new ObjectNormalizer(null, null, new PropertyAccessor(), new ReflectionExtractor());
$serializer = new Serializer(array($normalizer), array(new JsonEncoder()));
$lead = $serializer->deserialize($data, Lead::class, 'json');
I can’t understand what the problem is. It is expected using the addProduct() method deserializer should bypass the array and add all objects to Product class like in this case.
I decided using an ArrayDenormalizer, and a setter with PHPDoc indicating the data type, an array of Products [] objects. But why the method with addProduct () did not work - the question remains.
Deserialization:
$encoder = [new JsonEncoder()];
$extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]);
$normalizer = [new ArrayDenormalizer(), new ObjectNormalizer(null, null, null, $extractor)];
$serializer = new Serializer($normalizer, $encoder);
/** #var $lead Lead */
$lead = $serializer->deserialize($data,Lead::class,'json');
Setter for products in the Lead class:
/**
* #param Product[] $products
*/
public function setProducts(array $products)
{
$this->products = $products;
}
I'am trying to desirialize the following Json structure into a Profile object
{
"label": "lorem label",
"info": {
"name": "lorem name",
"title": "lorem title"
}
}
I have a class Profile
namespace App\Document;
class profile
{
/**
* #var string
*/
protected $label;
/**
* #var Info
*/
protected $info;
// getters and setters
}
and a class Info
namespace App\Document;
class Info
{
/**
* #var string
*/
protected name;
/**
* #var string
*/
protected title;
// getters & setters
}
my controller's code is the following
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use App\Document;
public function addProfile(Request $request, DocumentManager $dm, $profile_id)
{
$phpDocNormalizer = new ObjectNormalizer(null, null, null, new PhpDocExtractor());
$reflectionNormalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
$serializer = new Serializer([$phpDocNormalizer, $reflectionNormalizer], [new JsonEncoder()]);
$profile = $serializer->deserialize($request->getContent(), Profile::class, 'json');
return new JsonResponse(Response::HTTP_CREATED);
}
As the symfony docs says, i have the phpDocExtractor set and working, and the doc annotations are set too, the PropertyInfo Component is installed too. But i keep getting this NotNormalizableValueException:
The type of the "info" attribute for class "App\Document\Profile" must be one of "App\Document\Info" ("array" given).
I've been stuck with this one for a while now, any help is much appreciated.
public function addProfile(Request $request, DocumentManager $dm, $profile_id)
{
$extractors = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = new ObjectNormalizer(null, null, null, $extractors);
$serializer = new Serializer([$normalizer], [new JsonEncoder()]);
$profile = $serializer->deserialize($request->getContent(), Profile::class, 'json');
return new JsonResponse(Response::HTTP_CREATED);
}
You can find a more detailed example here : https://symfony.com/doc/current/components/property_info.html#usage