I'm using Symfony 5.4 and its Symfony/Serializer component.
I receive a JSON payload like the following one:
{
"name": "Truck",
"age": 23
}
I wish I could deserialize it to the following model, where the name JSON field would be mapped onto the lastname User class attribute:
class User {
protected lastname;
protected age;
}
My issue here is that the deserialization fails since there is no name field in my model.
Sure I could add a name field to my User model, and write its getter/setters as follow and use Serializer's group not to serialize the name field :
public function getName(): string
{
return $this->lastname;
}
public function setName(string $name): User
{
$this->lastname = $name;
return $this;
}
But this looks hackish. Anyway, on top of the aforementioned, I need any User entity to be serialized to :
{
"lastname": "Truck",
"age": 23
}
As a consequence, I can not use #SerializedName('name') here.
What would be the cleaner way to achieve this?
You can implement an AdvancedNameConverterInterface.
A very simple, naive implementation:
<?php declare(strict_types=1);
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
class UserNameConverter implements AdvancedNameConverterInterface
{
public function denormalize(string $propertyName, string $class = null, string $format = null, array $context = [])
{
if ($class === User::class && $propertyName === 'lastname') {
return 'name';
}
return $propertyName;
}
public function normalize(string $propertyName, string $class = null, string $format = null, array $context = []): string
{
return $propertyName;
}
}
Check for the docs here.
To enable it in the configuration, you should be able to do:
framework:
# ...
serializer:
name_converter: 'App\YourNamespace\UserNameConverter'
Related
What do I want to do: I want to build a WebScraper for different pages (webpageA, webpageB). I want to read the title and the teaser image. Since both pages use different tags to display the title, I have two patterns.
My goal: I want to try out the principle of inheritance on a practical example to understand it.
What have I done so far: My controller receives a request with a url. The controller checks whether the URL belongs to the page webpageA or webpageB. Now it dynamically creates an instance of the corresponding service: $service = new WebpageService[A || B]($url). Then I start the scraping with $service->startScraping().
Both services implement an interface in which I have the signatures getTitle() and getImage().
class WebpageAService implements IPageService
private string $url;
private array $data;
private $keyMap = [
'key' => 'mappendKey',
....
];
public function __construct($url) {
$this->url = $url;
}
public function startScraping() {
$this->data = // getTheDataFromApi()
return [
'title' => $this->getTitle(),
'image' => $this->getImage()
]
}
public function getTitle(): string
{
$key = 'title';
if (! $this->keyExists($key)) {
return "Please enter a $key";
}
return $this->getValue($key);
}
private function keyExists(string $key): bool
{
if (! isset($this->data[$this->keyMap[$key]])) {
$this->errorLog[$key] = "Error: $key with the mapped key: $this->keyMap[$key] not found on $this->url";
return false;
}
return true;
}
private function getValue(string $key): string
{
return strlen($this->data[$this->keyMap[$key]]) > 0 ?
$this->data[$this->keyMap[$key]] : $this->message['length'];
}
}
My question: Can I outsource the two private functions? So that I don't have to include them for each webpage[A || B] class? If so, would a trait make sense here? I am very grateful for your hints and comments. Thanks for your time! Max
I have encountered that in Authenticatable trait in laravel:
public function setRememberToken($value)
{
if (! empty($this->getRememberTokenName())) {
$this->{$this->getRememberTokenName()} = $value; //here
}
}
public function getRememberTokenName()
{
return $this->rememberTokenName;
}
I know the first $this will point out the class (Model) that the trait is used in. However,
What is the meaning of the second {$this}?
What does it actually do?
Why they did not simply say:
public function setRememberToken($value)
{
if (! empty($this->getRememberTokenName())) {
$this->rememberTokenName = $value;
}
}
This is complex curly syntax.
Basically if $this->getRememberTokenName() returns the string value six then the expression is essentially $this->six
The trait is using the value returned from getRememberTokenName(), which is really just the value of $this->rememberTokenName, as the name of the property in the model class that should hold the value. For instance, if $this->rememberTokenName is set to 'myToken', the setRememberTokenFunction is doing the equivalent of
$this->myToken = $value;
This is just a convoluted way of allowing the model class to configure the name of the variable that holds the token value.
<?php
trait Authenticatable
{
public function setRememberToken($value)
{
// If there is a token name...
if (!empty($this->getRememberTokenName()))
{
/*
* Set the property with the token name to the provided value
* Example: if $this->getRememberTokenName() returns 'myToken',
* this is equivalent to $this->myToken = $value
*/
$this->{$this->getRememberTokenName()} = $value;
}
}
public function getRememberTokenName()
{
return $this->rememberTokenName;
}
}
class Model
{
use Authenticatable;
private $myToken;
private $rememberTokenName = 'myToken';
public function __construct($myToken)
{
$this->myToken = $myToken;
}
public function getMyToken()
{
return $this->myToken;
}
}
$myModel = new Model('foo');
assert($myModel->getMyToken() == 'foo', 'Token should match constructor argument');
$myModel->setRememberToken('bar');
$updatedToken = $myModel->getMyToken();
assert($updatedToken == 'bar', 'Token should have been updated by trait');
I am receiving a payload that looks like this:
{
"date": "2019-03-14 14:48:26 +0000",
"events": [
"E09FDE82-4CAA-4641-87AF-6C092D6E71C1",
"AE12A6BC-DA37-4C37-BF49-DD0CE096AE00"
],
"location": null
}
The wrapper object is an Animal entity and the events is an array of UUIDs that belong to Event entities. These may or may not exist in the events table.
I want to be able to serialize this into an Animal entity using the symfony serializer like so:
$serializer = $this->get("serializer");
if($request->getMethod() == Request::METHOD_POST) {
$data = $request->getContent();
$entity = $serializer->deserialize($data, $this->type, 'json');
...
...
What I would like to do is during deserialization, I need to look for that particular key and iterate over it, creating new Events (or getting existing ones) and call the setter on the animal with these.
I have had a look at symfony normalizers but I don't think these are the right things? I made this but not sure where to go from here:
<?php
namespace App\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use App\Entity\Event;
class EventNormalizer implements NormalizerInterface {
private $normalizer;
public function __construct(ObjectNormalizer $normalizer){
$this->normalizer = $normalizer;
}
public function normalize($event, $format = null, array $context = [])
{
$data = $this->normalizer->normalize($event, $format, $context);
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
return $data instanceof Event;
}
}
According to the documentation, this is how you would edit existing values or add new ones but I have no idea how I would tell the normalizer that "hey, when you see this key, you're up, do your thing".
Any help appreciated.
Here you need a Denormalizer, try to implement DenormalizerInterface
class EventNormalizer implements NormalizerInterface, DenormalizerInterface {
...
public function denormalize($data, string $type, string $format = null, array $context = [])
{
// retrieve your events from $data and return the object (Animal) with related events
// $this->entityManager->find($data['events'][0]) ...
}
public function supportsDenormalization($data, string $type, string $format = null)
{
// your code here
}
}
can somebody try to explain me how to use multiple normalizers when serializing data from multiple classes with the Symfony serializer?
Lets say that I have the following classes:
class User
{
private $name;
private $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
// getters and setters
}
class Book
{
private $title;
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
}
And I want to serialize an user who has multiple books.
$first = new Book();
$first->setTitle('First book');
$second = new Book();
$second->setTitle('Second book');
$user = new User();
$user->setName('Person name');
$user->addBook($first);
$user->addBook($second);
dump($this->get('serializer')->serialize($user, 'json'));
die();
Let's say that I also want to include a hash when serializing a book, so I have the following normalizer:
class BookNormalizer implements NormalizerInterface
{
public function normalize($object, $format = null, array $context = array())
{
return [
'title' => $object->getTitle(),
'hash' => md5($object->getTitle())
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Book;
}
}
And I am getting the expected result:
{"name":"Person name","books":[{"title":"First book","hash":"a9c04245e768bc5bedd57ebd62a6309e"},{"title":"Second book","hash":"c431a001cb16a82a937579a50ea12e51"}]}
The problem comes when I also add a normalizer for the User class:
class UserNormalizer implements NormalizerInterface
{
public function normalize($object, $format = null, array $context = array())
{
return [
'name' => $object->getName(),
'books' => $object->getBooks()
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof User;
}
}
Now, the books aren't normalized using the previously given normalizer, and i get the following:
{"name":"Person name","books":[{},{}]}
I tried to find a way (documentation and other articles) to always call the normalizers for the given types (eg. always call the book normalizer when the type is Book, even if the data is nested and used in another normalizer) but could not succeed.
I think i have misunderstood something about normalizers but don't know what. Can somebody explain to is what i want possible and how to do it?
You have to use the NormalizerAwareTrait so you can access the normalizer for books
add interface
use trait
call normalize() method for books
code:
class UserNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function normalize($object, $format = null, array $context = array())
{
return [
'name' => $object->getName(),
'books' => $this->normalizer->normalize($object->getBooks(), $format, $context)
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof User;
}
}
I've got one Aggregate root - Product - that has few fields and some of them are objects, like Price. It looks like that ( of course it is simplified ):
Product
{
private $price;
public function __construct(Price $price)
{
$this->price = $price;
}
}
Price
{
private $currency;
private $amount;
public function __construct($currency, $amount)
{
$this->currency = $currency;
$this->amount= $amount;
}
}
This aggregate root doesn't contain "getPrice" method, it's not needed in Domain.
The issue:
I need to serialize this aggregate, but I would like to have it in this format:
Product.json
{
"priceCurrency": "GBP",
"priceAmount": 100
}
I've been trying with JMSSerializer, but can't really get it from config. This for example doesn't work:
Product.yml
Namespace\Product:
virtual_properties:
getAmount:
exp: object.price.amount
serialized_name: priceAmount
type: integer
getCurrency:
exp: object.price.currency
serialized_name: priceCurrency
type: string
I understand that it's due to the fact, that "exp" part is being used by Symfony Expression Language and from what I know it doesn't support getting values from private fields in any other way then through theirs methods. I also know that JMSSerializer itself supports that. I don't have to have field "getPrice" to serialize "price" field.
Question: Is there any way to achieve what I want just through config or do I have to write listeners on post_serialize event?
Use something like this:
<?php
class Property
{
protected $reflection;
protected $obj;
public function __construct($obj)
{
$this->obj = $obj;
$this->reflection = new ReflectionObject($obj);
}
public function set($name, $value)
{
$this->getProperty($name)->setValue($this->obj, $value);
}
public function get($name)
{
return $this->getProperty($name)->getValue($this->obj);
}
protected function getProperty($name)
{
$property = $this->reflection->getProperty($name);
$property->setAccessible(true);
return $property;
}
}
// example
class Foo
{
protected $bar = 123;
public function getBar()
{
return $this->bar;
}
}
$foo = new Foo();
echo 'original: '.$foo->getBar().PHP_EOL;
$prop = new Property($foo);
echo 'reflection - before changes: '.$prop->get('bar').PHP_EOL;
$prop->set('bar', 'abc');
echo 'after changes: '.$foo->getBar().PHP_EOL;