Symfony serialization: changing default DateTimeNormalizer format - symfony

I am trying to serialize an entity class which has some \DateTime fields. Everything works fine, but \DateTime objects are converted to string using the following format: "2019-10-21T01:05:12+00:00", while I would like to get just the date part: "2019-10-21".
Symfony documentation mentions default format but doesn't explain how to configure it:
DateTimeNormalizer This normalizer converts DateTimeInterface objects (e.g. DateTime and DateTimeImmutable) into strings. By default, it uses the RFC3339 format.
Is it possible to change the default DateTime normalization format and how?
Entity class:
class Fact
{
/**
* #ORM\Column(type="datetime", options={"default": "CURRENT_TIMESTAMP"})
* #Groups({"api"})
*/
private $created_on;
}
Normalization example:
use Symfony\Component\Serializer\SerializerInterface;
class FactController extends AbstractController
{
private $serializer;
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
public function view($id)
{
....
$data = array(
'fact' => $this->serializer->normalize($fact, null, ['groups'=> 'api']),
);
...
}
}

It turns out that '$context' array is passed down to normalize() function of each supported Normalizer. Built-in Normalizers define array keys they accept and their default values.
The relevant key in my case is 'datetime_format', which defaults to \DateTime::RFC3339. Format must be the one accepted by \DateTime::format() and \DateTime::createFromFormat() methods - these functions are used for normalization / denormalization.
Correct usage in my case is:
public function view($id)
{
....
$data = array(
'fact' => $this->serializer->normalize($fact, null, ['groups'=> 'api',
'datetime_format' => 'Y-m-d']),
);
...
}

Here is an answer in another question. Solution is:
services:
serializer.normalizer.datetime:
class: ‘Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
arguments
-
!php/const Symfony\Component\Serializer\Normalizer\DateTimeNormalizer::FORMAT_KEY: 'Y-m-d\TH:i:s.uP’
tags:
- { name: serializer.normalizer, priority: -910 }
Priority is taken from the original service, so, this solution will not have any side effects.

Related

Symfony3 deserialize

I have this entity:
AppBundle\Entity\Ciudad
class Ciudad{
...
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\ComunidadAutonoma")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_ccaa", referencedColumnName="id")
* })
*/
private $ccaa;
....
public function getCcaa()
{
return $this->ccaa;
}
public function setCcaa(ComunidadAutonoma $ccaa)
{
$this->ccaa = $ccaa;
}
}
And the other entity is:
AppBundle\Entity\ComunidadAutonoma
class ComunidadAutonoma{
properties
getters
setters
}
In a controller, I get data from a form, and I´m triying to deserialize the data into a Ciudad entity, but is getting me allways the same error:
Expected argument of type "AppBundle\Entity\ComunidadAutonoma", "integer" given
In the form data I send to the action in the controller, the value of the comunidadautonoma is the id of the selected option in a combo:
{
parameters...
ccaa:7,
parameters...
}
In my controller I have this:
<?php
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use AppBundle\Entity\Ciudad;
class CiudadController extends Controller
{
public function procesarAction(Request $request)
{
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$this->serializer = new Serializer($normalizers, $encoders);
$ciudad= $this->serializer->deserialize($parametros['parametros'], Ciudad::class, 'json');
}
}
Am I missing something?Do I need any special configuration to deserializer an entity with a relation?
You dont have to do anything if you properly configured a type. While creating a Form Type for your entity please add class name to your type like:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Ciudad::class,
]);
}
And please use english naming for your projects.
First of all, since you are sending form data to your controller you could use Form Type classes to leverage all the power of the Symfony Form Component that will all this job for you.
Answering your specific question (and assuming you cannot/don't want to use Symfony Form Component) this error is absolutely expected. As you can see in your setCcaa function declaration inside Ciudad class:
public function setCcaa(ComunidadAutonoma $ccaa)
Because of the type-hinting (ComunidadAutonoma $ccaa) setCcaa function expects an argument of type ComunidadAutonoma. Now when Symfony serializer tries to denormalize your json object it calls setCcaa function with argument the ccaa value provided in your json (in your example is 7) which happens to be an integer. So Symfony complains that you provide an integer instead of ComunidadAutonoma type.
In order to solve this problem you have to create and use your own normalizer so that you can transform this integer to the corresponding entity object from your database. Something like this:
class EntityNormalizer extends ObjectNormalizer
{
/**
* Entity manager
* #var EntityManagerInterface
*/
protected $em;
public function __construct(
EntityManagerInterface $em,
?ClassMetadataFactoryInterface $classMetadataFactory = null,
?NameConverterInterface $nameConverter = null,
?PropertyAccessorInterface $propertyAccessor = null,
?PropertyTypeExtractorInterface $propertyTypeExtractor = null
) {
parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);
// Entity manager
$this->em = $em;
}
public function supportsDenormalization($data, $type, $format = null)
{
return strpos($type, 'App\\Entity\\') === 0 && (is_numeric($data) || is_string($data));
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->em->find($class, $data);
}
}
What this normalizer does is that it checks if your data type (in this case $ccaa) is type of an entity and if the data value provided (in this case 7) is an integer, it transforms this integer to the corresponding entity object from your database (if existing).
To get this normalizer working you should also register it in your services.yaml configuration, with the appropriate tags like this:
services:
App\Normalizer\EntityNormalizer:
public: false
autowire: true
autoconfigure: true
tags:
- { name: serializer.normalizer }
You could also set the normalizer's priority but since the default priority value is equal to 0 when Symfony's built-in normalizers' priority is by default negative, your normalizer will be used first.
You could check a fully explained example of this in this fine article.

Symfony+JMS Serializer deserialize into existing object

I've been wondering if it's possible to use the JMS Serializer to deserialize JSON into an existing object.
Usually that would be useful for updating an existing object with new data that you have in a JSON format. Symfony's standard deserializer seems to offer that, but I can't seem to find anything about this with JMS. Have to use JMS though if I want the serializedName Annotation option.
The "workaround" is to deserialize and then use Doctrine's EntityManager to merge, but that only works so well, and you can't easily discern which fields are updated if the JSON doesn't contain every single field.
I have struggled to find the solution but finally found it and here we go:
your services.yaml
jms_serializer.object_constructor:
alias: jms_serializer.initialized_object_constructor
jms_serializer.initialized_object_constructor:
class: App\Service\InitializedObjectConstructor
arguments: ["#jms_serializer.unserialize_object_constructor"]
create class App\Service\InitializedObjectConstructor.php
<?php
declare(strict_types=1);
namespace App\Service;
use JMS\Serializer\Construction\ObjectConstructorInterface;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
class InitializedObjectConstructor implements ObjectConstructorInterface
{
private $fallbackConstructor;
/**
* #param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
*/
public function __construct(ObjectConstructorInterface $fallbackConstructor)
{
$this->fallbackConstructor = $fallbackConstructor;
}
/**
* {#inheritdoc}
*/
public function construct(
DeserializationVisitorInterface $visitor,
ClassMetadata $metadata,
$data,
array $type,
DeserializationContext $context
): ?object {
if ($context->hasAttribute('target') && 1 === $context->getDepth()) {
return $context->getAttribute('target');
}
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
}
in your controller or your service file
$object = $this->entityManager->find('YourEntityName', $id);
$context = new DeserializationContext();
$context->setAttribute('target', $object);
$data = $this->serializer->deserialize($request->getContent(), 'YourEntityClassName', 'json', $context);
So this can be done, I haven't fully worked out how, but I switched away from JMS again, so just for reference, since I guess it's better than keeping the question open for no reason:
https://github.com/schmittjoh/serializer/issues/79 and you might find more digging around the GitHub too.

Symfony custom form weird property access errors

I've got this strange problem, here is example usage of my custom ThingType class.
->add('photos', 'namespace\Form\Type\ThingType', [
'required' => false,
])
if the field name is photos everything works as expected, but if I change my entity field to let's say photosi, run generate entities, and change the form field name, this error is thrown:
Neither the property "photosi" nor one of the methods
"addPhotosus()"/"removePhotosus()", "setPhotosi()", "photosi()",
"__set()" or "__call()" exist and have public access in class
"AppBundle\Entity\Product".
I guess the problem comes from Symfony trying to generate getter method name for my entity. Why is this addPhotosus method name generated? How can I solve this?
EDIT:
I'm using model transformer when showing the data to the user.
$builder->addModelTransformer(new CallbackTransformer(
function ($imagesAsText) {
if (!$imagesAsText) {
return null;
}
$newImages = [];
foreach($imagesAsText as $img) {
$newImages[] = $img->getID();
}
return implode(',', $newImages);
},
function ($textAsImages) use ($repo) {
$images = [];
foreach(explode(',', $textAsImages) as $imgID) {
$img = $repo->findOneById($imgID);
if ($img) {
$images[] = $img;
}
}
return $images;
}
));
The actual field is TextType::class with entity ids in it for example 1,10,32,51. The model transformer transforms this data to entities. Setting 'data_class' to my form type seems irrelevant, because the actual form type is a part of entity. I mean I have Product entity and Photo entity, photos is array of photo entity. So in my ThingType, what data_class should I use, photo or product?
Thanks
The fist parameter of the add method for a form, should be one of the mapped attributes of the data_class of the form, usually selected inside the form as
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product'
));
}
That isn't related to the form name. So , you are trying to access to a "photosi" attribute inside your Product class.
Hope this help you.
Ok so for the first point you need to remember that Symfony is looking for setXX() and getXX()method in your entity for each entry of your form.
If you change your variable name you need to update the form :
->add('newName', XXType::class, [
'required' => false,
])
and you're entity by changing the variable
class Entity
{
/**
* #ORM\Column(type="string", length=255)
*/
private $newName;
public function getOldName(){
return $this->$oldName;
}
public function setOldName(oldName){
$this->oldName = $oldName;
return $this
}
}
then run the command
php bin/console make:entity --regenerate
and symfony will upload your entity by itself
class Entity
{
/**
* #ORM\Column(type="string", length=255)
* #SerializedName("title")
* #Groups({"calendar"})
*/
private $newName;
public function getOldName(){
return $this->$oldName;
}
public function setOldName($oldName){
$this->oldName = $oldName;
return $this
}
public function getNewName(){
return $this->newName;
}
public function setNewName($newName){
$this->newName = $newName;
return $this
}
note that the old get and set method are not deleted by the script
note as well that in your specific case of photosi, symfonyguess that the "i" is a plural mark and look for addPhotosus() methods
For the edit it looks very unclear and has nothing to do with the first question. Consider reading : doc on collectionType

Symfony2 Form with custom FormType calls DataTransformer with same data in both directions

I have made a new FormType and it extends the entity type via
//...
public function getParent()
{
return 'entity';
}
Which lead my edit form to complain that an integer was not My/Entity/Type and I need a data transformer. So I created one. This is the abbreviated version (it's just the basic tutorial version)
//...
public function reverseTransform($val)
{
// Entity to int
return $val->getId();
}
public function transform($val)
{
// Int to Entity
return $repo->findOneBy($val);
}
//...
Then added it to my form type
//...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new IdToMyModelTransformer($this->em));
}
This fixed me viewing my form, but now when I submit the form with an entity picked from my custom widget it tries to call transform not reverseTransform with the $val as an int the ->getId() fails on a non-object.
I can't figure out the correct way of doing this. If I use 'choice' as my widget parent I get a different set of issues (choice default constraints triggered saying it is invalid data?)
I need an entity passed to my widget so it can extract the meta data for display, but I can't post an entity back of course. How do I tell the form that?
Tried setting 'data_class' => null but no joy. Checking network tab shows the value is sent correctly when posting the form.
Update 1
So I re-read the DataTransformer page and that diagram got me thinking, especially after rubber-duck programming above, I ask the form for Entity but expect it to receive ints.. so I actually need a unidirectional transformer, ViewTransformer -> Get entity for display, get posted an int from widget, don't transform it just pass straight through. Which works and I just get the "invalid data" error on update.
Now I have in my Transformer:
public function transform($val)
{
// Int to Entity
return $repo->findOneBy($val);
}
public function reverseTransform($val)
{
// Do nothing
return $val;
}
Update 2
That seems to have fixed it now, although for some reason if I post int 2 in my form the string "2/" is sent to my transformer. Any ideas on that? FOr now I'm cleaning the string in transformer, but seems like it just shouldnt be happening.
By what I'm seeing in your transformer class you're not implementing the code right. This should be the correct implementation:
namespace App\YourBundle\Form\DataTransformer;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IdToMyModelTransformer implements DataTransformerInterface
{
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em) {
$this->em = $em;
}
/**
* Transforms a value from the original representation to a transformed representation.
*
* This method is called on two occasions inside a form field:
*
* 1. When the form field is initialized with the data attached from the datasource (object or array).
* 2. When data from a request is submitted using {#link Form::submit()} to transform the new input data
* back into the renderable format. For example if you have a date field and submit '2009-10-10'
* you might accept this value because its easily parsed, but the transformer still writes back
* "2009/10/10" onto the form field (for further displaying or other purposes).
*
* This method must be able to deal with empty values. Usually this will
* be NULL, but depending on your implementation other empty values are
* possible as well (such as empty strings). The reasoning behind this is
* that value transformers must be chainable. If the transform() method
* of the first value transformer outputs NULL, the second value transformer
* must be able to process that value.
*
* By convention, transform() should return an empty string if NULL is
* passed.
*
* #param mixed $object The value in the original representation
*
* #return mixed The value in the transformed representation
*
* #throws TransformationFailedException When the transformation fails.
*/
public function transform($object) {
if (null === $object) {
return null;
}
return $object->getId();
}
/**
* Transforms a value from the transformed representation to its original
* representation.
*
* This method is called when {#link Form::submit()} is called to transform the requests tainted data
* into an acceptable format for your data processing/model layer.
*
* This method must be able to deal with empty values. Usually this will
* be an empty string, but depending on your implementation other empty
* values are possible as well (such as empty strings). The reasoning behind
* this is that value transformers must be chainable. If the
* reverseTransform() method of the first value transformer outputs an
* empty string, the second value transformer must be able to process that
* value.
*
* By convention, reverseTransform() should return NULL if an empty string
* is passed.
*
* #param mixed $categoryId The value in the transformed representation
*
* #return mixed The value in the original representation
*
* #throws TransformationFailedException When the transformation fails.
*/
public function reverseTransform($id) {
if (!$id || $id <= 0) {
return null;
}
if(!ctype_digit($id)){
throw new TransformationFailedException();
}
$repo = $this->em->getRepository('...');
$result = $repo->findOneBy(array('id' => $id));
if (null === $result) {
throw new TransformationFailedException(
sprintf(
'Entity with id does not exist!',
$id
)
);
}
return $result;
}
}
In the IdToMyIntType you would have something like this:
namespace App\YourBundle\Form\Type;
use App\YourBundle\Form\DataTransformer\IdToMyModelTransformer ;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class IdToMyModelType extends AbstractType {
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct( EntityManager $em ) {
$this->em = $em;
}
public function buildForm( FormBuilderInterface $builder, array $options ) {
$transformer = new IdToMyModelTransformer ( $this->em );
$builder->addModelTransformer( $transformer );
}
public function setDefaultOptions( OptionsResolverInterface $resolver ) {
$resolver->setDefaults(array('invalid_message' => 'Something went wrong message.'));
}
public function getParent() {
return 'entity';
}
public function getName() {
return 'id_to_model_type';
}
}
I would suggest you check out the DataTransformerInterface and read the documentation over the methods. It'll briefly explain what is that method expected to do. Also, in case you have problems implementing it, you can always check the official documentation, which contains a working example and build up from there.
As per my last update I realised, because I was only using my form data to display the currently saved entity relation (the rest is provided by ajax) and not in the same format the form would be receiving it in it lead to some confusion.
To follow the tutorials wording:
Model data
This was all to remain as-is (No model datatransformer needed)
Norm data
No changes
View data (unidirection transformation required)
Transform()
ID to Entity so widget can access other properties
ReverseTransform()
Posted ID is in correct format so we just return it
Code
Very simplified:
private $om;
public function __construct (ObjectManager om)
{
$this->om = $om;
}
public function transform($val)
{
// Int to Entity
return $om->getRepository('MyBundle:EntityName')->findOneBy($val);
}
public function reverseTransform($val)
{
// Do nothing
return $val;
}
Hopefully that helps anyone else who lets their requirements confuse them!

JMSSerializerBundle - preserve relation name

I'm using Symfony2 with JMSSerializerBundle. And I'm new with last one =) What should I do in such case:
I have Image model. It contains some fields, but the main one is "name". Also, I have some models, which has reference to Image model. For example User and Application. User model has OneToOne field "avatar", and Application has OneToOne field "icon". Now, I want to serialize User instance and get something like
{
...,
"avatar": "http://example.com/my/image/path/image_name.png",
....
}
Also, I want to serialize Application and get
{
...,
"icon": "http://example.com/my/image/path/another_image_name.png",
...
}
I'm using #Inline annotation on User::avatar and Application::icon fields to reduce Image object (related to this field) to single scalar value (only image "name" needed). Also, my Image model has ExclusionPolicy("all"), and exposes only "name" field. For now, JMSSerializer output is
(For User instance)
{
...,
"name": "http://example.com/my/image/path/image_name.png",
...
}
(For Application instance)
{
...,
"name": "http://example.com/my/image/path/another_image_name.png",
...
}
The question is: How can I make JMSSerializer to preserve "avatar" and "icon" keys in serialized array instead of "name"?
Finally, I found solution. In my opinion, it is not very elegant and beautiful, but it works.
I told to JMSSerializer, that User::avatar and Application::icon are Images. To do that, I used annotation #Type("Image")
//src\AppBundle\Entity\User.php
//...
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Image")
* #ORM\JoinColumn(name="avatar", referencedColumnName="id")
*
* #JMS\Expose()
* #JMS\Type("Image")
*/
private $avatar;
//...
//src\AppBundle\Entity\Application.php
//...
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Image")
* #ORM\JoinColumn(name="icon", referencedColumnName="id")
*
* #JMS\Expose()
* #JMS\Type("Image")
*/
private $icon;
//...
I implemented handler, which serializes object with type Image to json.
<?php
//src\AppBundle\Serializer\ImageTypeHandler.php
namespace AppBundle\Serializer;
use AppBundle\Entity\Image;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use Symfony\Component\HttpFoundation\Request;
class ImageTypeHandler implements SubscribingHandlerInterface
{
private $request;
public function __construct(Request $request) {
$this->request = $request;
}
static public function getSubscribingMethods()
{
return [
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => 'Image',
'method' => 'serializeImageToWebPath'
]
];
}
public function serializeImageToWebPath(JsonSerializationVisitor $visitor, Image $image = null, array $type, Context $context)
{
$path = $image ? "http://" . $this->request->getHost() . "/uploads/images/" . $image->getPath() : '';
return $path;
}
}
And the last step is to register this handler. I also injected request service to generate full web path to image in my handler.
app.image_type_handler:
class: AppBundle\Serializer\ImageTypeHandler
arguments: ["#request"]
scope: request
tags:
- { name: jms_serializer.subscribing_handler }
Also, you can use this workaround, to modify serialized data in post_serialize event.

Resources