I make a REST API on Symfony4, So I would like to Serialize my Entity with the default serializer of Symfony4.
But my entities have unusual attribute names that make serializer give me bad result.
I tried to implement NameConverterInterface and also tried CamelCaseToSnakeCaseNameConverter without a good result...
Every entity on my application have this kind of attribute so a solution with #annotation can't help me
class Product implements EntityInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer", name="PROD_PKEY")
*/
private $PROD_PKEY;
/**
* #ORM\Column(type="string", length=50)
*/
private $PROD_Name;
/**
* #ORM\Column(type="string", length=50)
*/
private $PROD_Code;
And how I use the serializer :
$product = new Product();
$product->setPRODName("Name");
$product->setPRODCode("Code");
$json = $this->serializer->serialize($product, 'json');
The content of $json is :
{
"pRODName": "Name",
"pRODCode": "Code",
}
but I expect somethings like this :
{
"PROD_Name": "Name",
"PROD_Code": "Code",
}
Simply equal to my attributes names in my entity, I don't understand why first letter get lowercase and my underscore get out...
Thanks for your help !
In Symfony you can implement a custom NameConverter to transform fields names in your json representation.
Something along these lines should do the trick:
<?php namespace App\Service;
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
class YourCustomNameConverter implements AdvancedNameConverterInterface
{
public function normalize($propertyName, string $class = null, string $format = null, array $context = [])
{
preg_match('/^([a-z]?[A-Z]+)([A-Z]{1}[_a-zA-Z]+)$/', $propertyName, $matches);
if (strstr($propertyName, 'PKEY')) {
return ucfirst(substr_replace($propertyName, '_', -4, 0));
} elseif (count($matches)) {
array_shift($matches);
$matches[0] = ucfirst($matches[0]);
return implode('_', $matches);
} else {
return $propertyName;
}
}
public function denormalize($propertyName, string $class = null, string $format = null, array $context = [])
{
return $propertyName;
}
}
I think you might need to create a custom serializer, I often use jmsserializer bundle and had no issues with it
How to use JMSSerializer with symfony 4.2
https://symfony.com/doc/current/serializer/custom_normalizer.html
Related
I am looking how to add a dynamic variable into an API Platform #ApiProperty annotation.
I found that Symfony allows that but it does not seem to work in API Platform annotations.
For example :
/**
* Redirection URL.
*
* #Groups({"authorization_code_login_write", "authorization_code_logout_write"})
* #ApiProperty(
* attributes={
* "openapi_context"={
* "type"="string",
* "example"="%app.auth.default.redirect%"
* }
* }
* )
*/
protected ?string $redirectionUrl = null;
%app.auth.default.redirect% is not replaced by the container parameter with the same name.
How should I do ?
At first sight, I see here only the one way - to create your own attribute in openapi_context, let's say my_redirect_example.
Smth like this, in example:
"openapi_context"={
"type"="string",
"my_redirect_example"=true
}
Then you need to decorate like in documentation
Smth, like that:
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decorated->normalize($object, $format, $context);
$redirectUrl = .... # your own logic to get this dynamical value
foreach ($docs['paths'] as $pathName => $path) {
foreach ($path as $operationName => $operation) {
if ($operation['my_redirect_example'] ?? false) {
$docs['paths'][$pathName][$operationName]['example'] = $redirectUrl;
}
}
}
return $docs;
}
It should work. Anyway - it is just an example (I didn't test it), just to understanding how you can handle it.
Sure, you can replace true value with your own and use it inside the if statement to get it depending on some yours own logic.
The way to go is to follow the documentation to decorate the Swagger Open API generator service (https://api-platform.com/docs/core/swagger/#overriding-the-openapi-specification).
Add your own service :
# api/config/services.yaml
services:
'App\Swagger\SwaggerDecorator':
decorates: 'api_platform.swagger.normalizer.api_gateway'
arguments: [ '#App\Swagger\SwaggerDecorator.inner' ]
autoconfigure: false
Then create you service class :
<?php
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Custom Swagger decorator to remove/edit some API documentation information.
*/
final class SwaggerDecorator implements NormalizerInterface
{
/**
* Decorated base Swagger normalizer.
*
* #var NormalizerInterface
*/
protected NormalizerInterface $decorated;
/**
* SwaggerDecorator constructor.
*
* #param NormalizerInterface $decorated
*/
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
/**
* {#inheritDoc}
*/
public function normalize($object, string $format = null, array $context = [])
{
$docs = $this->decorated->normalize($object, $format, $context);
$docs['components']['schemas']['authorization-authorization_code_login_write']['properties']['redirectionUrl']['example'] = 'https://example.com/my-dynamic-redirection';
$docs['components']['schemas']['authorization:jsonld-authorization_code_login_write']['properties']['redirectionUrl']['example'] = 'https://example.com/my-dynamic-redirection';
return $docs;
}
/**
* {#inheritDoc}
*/
public function supportsNormalization($data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
}
You'll just have to find which keys to use, browsing the schemas can help on your Swagger UI. In my example, authorization is the short name of my API resource entity and authorization_code_login_write is the denormalization context value of the operation.
And here you go :
Of course, the ideal solution will iterate over all schemas and replace found configuration parameters with their real values. Maybe this feature could be done in API Platform itself (Follow issue : https://github.com/api-platform/api-platform/issues/1711)
When I deserialize my doctrine entity, the initial object is constructed/initiated correctly, however all child relations are trying to be called as arrays.
The root level object's addChild(ChildEntity $entity) method is being called, but Symfony is throwing an error that addChild is receiving an array and not an instance of ChildEntity.
Does Symfony's own serializer have a way to deserialize nested arrays (child entities) to the entity type?
JMS Serializer handles this by specifying a #Type("ArrayCollection<ChildEntity>") annotation on the property.
I believe the Symfony serializer attempts to be minimal compared to the JMS Serializer, so you might have to implement your own denormalizer for the class. You can see how the section on adding normalizers.
There may be an easier way, but so far with Symfony I am using Discriminator interface annotation and type property for array of Objects. It can also handle multiple types in one array (MongoDB):
namespace App\Model;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/**
* #DiscriminatorMap(typeProperty="type", mapping={
* "text"="App\Model\BlogContentTextModel",
* "code"="App\Model\BlogContentCodeModel"
* })
*/
interface BlogContentInterface
{
/**
* #return string
*/
public function getType(): string;
}
and parent object will need to define property as interface and get, add, remove methods:
/**
* #var BlogContentInterface[]
*/
protected $contents = [];
/**
* #return BlogContentInterface[]
*/
public function getContents(): array
{
return $this->contents;
}
/**
* #param BlogContentInterface[] $contents
*/
public function setContents($contents): void
{
$this->contents = $contents;
}
/**
* #param BlogContentInterface $content
*/
public function addContent(BlogContentInterface $content): void
{
$this->contents[] = $content;
}
/**
* #param BlogContentInterface $content
*/
public function removeContent(BlogContentInterface $content): void
{
$index = array_search($content, $this->contents);
if ($index !== false) {
unset($this->contents[$index]);
}
}
Context
I found a lot of questions about partial API response with FOSRest and all the answers are based on the JMS serializer options (exlude, include, groups, etc). It works fine but I try to achieve something less "static".
Let's say I have a user with the following attributes: id username firstname lastname age sex
I retrieve this user with the endpoint GET /users/{id} and the following method:
/**
* #View
*
* GET /users/{id}
* #param integer $user (uses ParamConverter)
*/
public function getUserAction(User $user) {
return $user;
}
The method returns the user with all his attributes.
Now I want to allow something like that: GET /users/{id}?attributes=id,username,sex
Question
Did I missed a functionality of FOSRestBUndle, JMSserializer or SensioFrameworkExtraBundle to achieve it automatically? An annotation, a method, a keyword in the request or something else?
Otherwise, what is the best way to achieve it?
Code
I thought to do something like that:
/**
* #View
* #QueryParam(name="attributes")
*
* GET /users/{id}
*
* #param integer $user (uses ParamConverter)
*/
public function getUserAction(User $user, $attributes) {
$groups = $attributes ? explode(",", $attributes) : array("Default");
$view = $this->view($user, 200)
->setSerializationContext(SerializationContext::create()->setGroups($groups));
return $this->handleView($view);
}
And create a group for each attribute:
use JMS\Serializer\Annotation\Groups;
class User {
/** #Groups({"id"}) */
protected $id;
/** #Groups({"username"}) */
protected $username;
/** #Groups({"firstname"}) */
protected $firstname;
//etc
}
My implementation based on Igor's answer:
ExlusionStrategy:
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;
class FieldsExclusionStrategy implements ExclusionStrategyInterface {
private $fields = array();
public function __construct(array $fields) {
$this->fields = $fields;
}
public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext) {
return false;
}
public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext) {
if (empty($this->fields)) {
return false;
}
if (in_array($property->name, $this->fields)) {
return false;
}
return true;
}
}
Controller:
/**
* #View
* #QueryParam(name="fields")
*
* GET /users/{id}
*
* #param integer $user (uses ParamConverter)
*/
public function getUserAction(User $user, $fields) {
$context = new SerializationContext();
$context->addExclusionStrategy(new FieldsExclusionStrategy($fields ? explode(',', $fields) : array()));
return $this->handleView($this->view($user)->setSerializationContext($context));
}
Endpoint:
GET /users/{id}?fields=id,username,sex
You can do it like that through groups, as you've shown. Maybe a bit more elegant solution would be to implement your own ExclusionStrategy. #Groups and other are implementations of ExclusionStrategyInterface too.
So, say you called your strategy SelectFieldsStrategy. Once you implement it, you can add it to your serialization context very easy:
$context = new SerializationContext();
$context->addExclusionStrategy(new SelectFieldsStrategy(['id', 'name', 'someotherfield']));
I have a json object that I received by making a get API call. I make this call to receive a list of objects. It's a list of post... So I have an array of Post Objects.
Here the output :
{
"total":2,
"data":[
{
"id":2,
"user":{
"id":1,
"username":"sandro.tchikovani"
},
"description":"cool",
"nb_comments":0,
"nb_likes":0,
"date_creation":"2014-04-13T20:07:34-0700"
},
{
"id":1,
"user":{
"id":1,
"username":"sandro.tchikovani",
},
"description":"Premier pooooste #lol",
"nb_comments":0,
"nb_likes":0,
"date_creation":"2014-04-13T15:15:35-0700"
}
]
}
I would like to deserialize the data part...
The problem is that the Serializer in Symfony gives me an error ...
The error that I have :
Class array<Moodress\Bundle\PosteBundle\Entity\Poste> does not exist
How I do deserialize :
$lastPosts = $serializer->deserialize($data['data'], 'array<Moodress\Bundle\PosteBundle\Entity\Poste>', 'json');
How can I deserialze the data array... To have an array of Postes. I want to give to my view .twig an array Poste... I did precise the type when I deserialize... So I can't find what is the problem...
Thanks.
I think the best solution here is to create new PosteResponse class, like this one:
namespace Moodress\Bundle\PosteBundle\Response;
use JMS\Serializer\Annotation\Type;
class PosteResponse
{
/**
* #Type("integer")
*/
private $total;
/**
* #Type("array<Moodress\Bundle\PosteBundle\Entity\Poste>")
*/
private $data;
//getters here
}
and deserialize your response to that class:
$response = $serializer->deserialize(
$json,
'Moodress\Bundle\PosteBundle\Response\PosteResponse',
'json'
);
$posts = $response->getData();
That WILL do the trick, and it doesn't require you to decode and encode your json manually which is riddiculous in my opinion.
Since Symfony Serializer Component 2.8 to deserialize array of objects:
$persons = $serializer->deserialize($data, 'Acme\Person[]', 'json');
https://symfony.com/doc/master/components/serializer.html#handling-arrays
A less than ideal solution that I found was to first decode and then encode the json data again at the node that represents the data array. For example in your case:
$json = json_decode($json);
$json = json_encode($json->data);
$serializer->deserialize($json, 'array<Moodress\Bundle\PosteBundle\Entity\Poste>', 'json');
There must be a better solution than this but this seems more elegant than the above solution of de-serialising json.
The error is pretty clear. Your string does not match any existant class.
The example in official documentation says:
$person = $serializer->deserialize($data,'Acme\Person','xml');
In your case it should be more like:
$person = $serializer->deserialize($data['data'],'Moodress\Bundle\PosteBundle\Entity\Poste','json');
Update:
Ok then.
First, your json file does not seem to be valid (use http://jsonlint.com/ to test it). Be careful of that.
Second, you will have to fetch your json as an array with
$data = json_decode($yourJsonFile, true);
and then you can access to each 'data' array with
foreach($data['data'] as $result)
{
/* Here you can hydrate your object manually like:
$person = new Person();
$person->setId($user['id']);
$person->setDescription($user['description']);
Or you can use a denormalizer. */
}
I would make something like this
class PostsModel
{
/**
* #var int
*/
private $total;
/**
* #var PostModel[]
*/
private $data;
}
class PostModel
{
/**
* #var int
*/
private $id;
/**
* #var UserModel
*/
private $user;
/**
* #var string
*/
private $description;
/**
* #var int
*/
private $nb_comments;
/**
* #var int
*/
private $nb_likes;
/**
* #var \DateTime
*/
private $date_creation;
}
class UserModel
{
/**
* #var int
*/
private $id;
/**
* #var string
*/
private $username;
}
And in controller
$posts = $this->serializer->deserialize($data, PostsModel::class, 'json');
And this will return $postsModel with $data property which will have your array of entities
In case someone will be searching how to decode an array of objects using Symfony Serializer:
use Moodress\Bundle\PosteBundle\Entity\Poste;
// your json data
$data = '{
"total":2,
"data":[
{...},
{...}
]
}';
$lastPosts = $serializer->deserialize(
$data['data'],
'Poste[]', // or Poste::class.'[]',
'json'
);
Notice that you need to add [] after your class name, in this way Symfony will recognize your json data as an array of objects.
I am looking for a way to convert an array to doctrine Entity. I am using doctrine 2.
I have an entity class like:
class User
{
/**
* #Id
* #Column(type="integer", nullable=false)
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #Column(type="string", length=255, unique=true, nullable=false)
*/
protected $email;
/**
* #Column(type="string", length=64, nullable=false)
*/
protected $password;
/**
* #var DateTime
* #Column(type="datetime", nullable=false)
*/
protected $dob;
//getter and setters
}
When I post data from a html form, I want to convert the post array to the User entity. So I have an array like
$userAsArray = array("email"=>"abc#xyz.com","password"=>"hello","dob"=>"10\20\1990");
$user = new User();
covert($userAsArray,$user) // I am looking for something like this
I am looking for a generic way to accomplish this. I have tried something like this:
function fromArray(array $array,$class){
$user = new $class();
foreach($array as $key => $field){
$keyUp = ucfirst($key);
if(method_exists($user,'set'.$keyUp)){
call_user_func(array($user,'set'.$keyUp),$field);
}
}
return $user;
}
But the problem is it sets everything as string. But for the date I want to have it as a DateTime object. Any help?
what if one of your array elements is a foreign key ? before setting entity properties, you might need to prepare foreign key attributes. This is how I accomplish similar task:
Extend Repository
<?php
namespace My\Doctrine;
use Doctrine\ORM\EntityRepository;
class Repository extends EntityRepository
{
/**
* Prepare attributes for entity
* replace foreign keys with entity instances
*
* #param array $attributes entity attributes
* #return array modified attributes values
*/
public function prepareAttributes(array $attributes)
{
foreach ($attributes as $fieldName => &$fieldValue) {
if (!$this->getClassMetadata()->hasAssociation($fieldName)) {
continue;
}
$association = $this->getClassMetadata()
->getAssociationMapping($fieldName);
if (is_null($fieldValue)) {
continue;
}
$fieldValue = $this->getEntityManager()
->getReference($association['targetEntity'], $fieldValue);
unset($fieldValue);
}
return $attributes;
}
}
Create parent Entity class :
namespace My\Doctrine;
class Entity
{
/**
* Assign entity properties using an array
*
* #param array $attributes assoc array of values to assign
* #return null
*/
public function fromArray(array $attributes)
{
foreach ($attributes as $name => $value) {
if (property_exists($this, $name)) {
$methodName = $this->_getSetterName($name);
if ($methodName) {
$this->{$methodName}($value);
} else {
$this->$name = $value;
}
}
}
}
/**
* Get property setter method name (if exists)
*
* #param string $propertyName entity property name
* #return false|string
*/
protected function _getSetterName($propertyName)
{
$prefixes = array('add', 'set');
foreach ($prefixes as $prefix) {
$methodName = sprintf('%s%s', $prefix, ucfirst(strtolower($propertyName)));
if (method_exists($this, $methodName)) {
return $methodName;
}
}
return false;
}
}
Usage, a method in your repo:
$entity = new User();
$attributes = array(
"email" =>"abc#xyz.com",
"password" =>"hello",
"dob" =>"10\20\1990"));
$entity->fromArray($this->prepareAttributes($attributes));
$this->getEntityManager()->persist($entity);
$this->getEntityManager()->flush();
Why not write your setDob() method to detect a string and convert it if necessary
public function setDob($dob) {
if (!$dob instanceof DateTime) {
$dob = new DateTime((string) $dob); // or however you want to do the conversion
}
$this->dob = $dob;
}
You are trying to de-serialize
I would check out the Zend Framework 2 Stdlib component. You do not need to use the whole framework.
The Hydrators, specifically the DoctrineModule\Stdlib\Hydrator\DoctrineObject, do what you are asking.