Symfony SerializedName equivalent for collection attributes while denormalizing - php

I am having a json request
{
applicant_info:{
"email": "jhon.doe#gmail.com",
"given_name": "jhon",
"family_name": "doe"
},
productId: 12
}
Having a dto class as below:
class Dto
{
/**
* #var int
* #SerializedName("product_id")
*/
public $productId;
/**
* #var Collection
* #SerializedName("applicant_info")
*/
public $applicantInfo = [
'email',
'givenName',
'familyName'
];
}
Now when I use below to deserialize I am not sure how to convert given_name and family_name from request to
givenName and familyName like I have converted product_id and applicant_info to productId and applicantInfo using #SerializedName
$serializer->deserialize(
$request->getContent(),
Dto::class,
self::FORMAT_JSON
);
Any help will be really appreciated

Related

How to prevent lazy loading of vulnarable entities in Symfony

My question is rather simple, but I didn't find any clues on the Internet after googling for one hour.
I'm trying to build an Symfony API, but when returning json output, it lazy loads, every relation into the output. While this is not such a big deal (in most cases), its really bad when it does this trick with user information. So everything (password, email, etc.) is displayed.
My question is: Is it possible to mark an entity in doctrine, as protected, so the autoload will not be made, with this entity? In some cases it comes pretty handy but this is a big flaw. If its not possible to mark an entity, is it possible to deactivate it completely, or on an Collection Element?
Thanks in advance
EDIT:
class User implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password;
/**
* #ORM\OneToOne(targetEntity="App\Entity\Profile", mappedBy="user", cascade={"persist", "remove"})
*/
private $profile;
getters and setters are there.
And there is a Profile class, that is the interface, for all relations. It has an 1to1 relation.
class Profile
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity="App\Entity\User", inversedBy="profile", cascade={"persist", "remove"})
* #ORM\JoinColumn(nullable=false)
*/
private $user;
getters and setters are there to.
class Event
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetime")
*/
private $date;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Profile", inversedBy="ownedEvents")
* #ORM\JoinColumn(nullable=false)
*/
private $profile;
/**
* #ORM\OneToMany(targetEntity=Post::class, mappedBy="event", orphanRemoval=true)
*/
private $posts;
The problem ist, that this profile is loaded, and with it the user...
The following is the controller function. But the serialization is happening in an extra method.
public function getUnreactedEvents(): JsonResponse{
$events = $this->getDoctrine()
->getManager()
->getRepository(Event::class)
->getUnreactedEvents($this->profileUtils->getLoggedInProfileFromDatabase()->getId());
return new JsonResponse($this->eventUtils->eventsToArray($events));
}
here is the to array function. (There is a base class so there are two mathods:
\Utils class:
\\Utils class:
public function eventsToArray($events): array{
return $this->toArray($events, array("usrEvntSts"));
}
\\Base class:
protected function toArray($objects, $fieldsToBeRemoved): array{
$normalizers = [new DateTimeNormalizer(), new ObjectNormalizer()];
$serializer = new Serializer($normalizers);
if(!is_array($objects)){
$objects = array($objects);
}
//normalizes the objects object, for circular references, returns id of the object
//doctrine comes with own array format
$objectsArray = $serializer->normalize($objects, 'array', [
'circular_reference_handler' => function ($object) {
return $object->getId();
}
]);
//some keys have to be erased from the event response
foreach ($objectsArray as $key => $object) {
if (method_exists($objects[0], "getProfile")){
/** #var Profile $profile */
$profile = $objects[$key]->getProfile();
unset($objectsArray[$key]["profile"]);
$objectsArray[$key]["profile"]['id'] = $profile->getId();
}
foreach ($fieldsToBeRemoved as $field){
unset($objectsArray[$key][$field]);
}
}
return $objectsArray;
}
}
As you see, my first idea was to just delete the field. But afer I added an new entity relation (posts), which has an owner profile too. The user class is loaded again...
Output:
{
"id": 1,
"name": "xcvxycv",
"date": "2020-06-28T18:08:55+02:00",
"public": false,
"posts": [
{
"id": 1,
"date": "2020-06-30T00:00:00+02:00",
"content": "sfdnsdfnslkfdnlskd",
"profile": {
"id": 2,
"user": {
"id": 2,
"email": "alla",
"username": "alla",
"roles": [
"ROLE_USER"
],
"password": "$argon2id$v=19$m=65536,t=4,p=1$a01US1dadGFLY05Lb1RkcQ$npmy0HMf19Neo/BnMqXGwkq8AZKVSCAEmDz8mVHLaQ0",
"salt": null,
"apiToken": null,
"profile": 2
},
"username": "sdfsdf",
"usrEvntSts": [],
"ownedEvents": [
{
"id": 3,
"name": "blaaaa",
"date": "2020-06-28T18:08:55+02:00",
"profile": 2,
"public": false,
"usrEvntSts": [],
"posts": [
{
"id": 2,
"date": "2020-06-30T00:00:00+02:00",
"content": "sfdnsdfnslkfdnlskd",
"profile": 2,
"event": 3,
"comments": []
}
]
},
And it goes on and on and on....
I would suggest to use JMSSerializerBundle for that. It is a widely used bundle, also in huge API's. You can exactly configure which properties should be exposed and which not. You can also build groups for exposing properties and use a specific exclusion strategy. Check the documentation for further information.
Hint: also check the Limiting serialization depth for deep nested objects.

Problem with use Model in Property annotation. Should be model not array

"nelmio/api-doc-bundle": "^3.6#dev"
I need response with model with property product which eque one model Product and property count, but when I use Mode annotation I faced with probem, response model generated like array Product. What I'm doing wrong ?
* #SWG\Response(
* response=200,
* description="Json collection object Products",
* #SWG\Schema(
* type="object",
* properties={
* #SWG\Property(property="product",
* #Model(type=Product::class, groups={Product::SERIALIZED_GROUP_LIST})),
* #SWG\Property(property="count", type="integer")
* }
* )
* )
{
"product": [
{
"id": 0
}
],
"count": 0
}
I expected result like this:
{
"product":
{
"id": 0
},
"count": 0
}

Laravel CRUD testing

In my Laravel application I am trying to get into feature testing and have started with a model called Announcement.
One test I'm running is whether a user can create an instance of Announcement and persist it to the database.
My test is as follows:
/** #test */
public function a_user_can_create_an_announcement()
{
$this->withoutExceptionHandling();
$this->setupPermissions();
$announcement = factory(Announcement::class)->raw();
$this->actingAs(factory(User::class)->create())->get(route('announcements.index'))->assertStatus(200);
$this->post(route('announcements.store', $announcement))->assertStatus(302);
$this->assertDatabaseHas('announcements', $announcement);
}
Now as far as I understand factory(Announcement::class)->raw(); returns a new Announcement as an array using the relevant model factory.
I then make a request to my store endpoint with the data array and expect to redirected so I add the following:
$this->post(route('announcements.store', $announcement))->assertStatus(302);
The final line is to check the announcement was written to the database table called announcements
I get the following error from the test case:
1) Tests\Feature\AnnouncementsTest::a_user_can_create_an_announcement
Failed asserting that a row in the table [announcements] matches the attributes {
"message": "King. 'When did you.",
"message_details": "The Caterpillar.",
"author": "beatrice-herzog",
"status": "pending",
"published_at": null,
"created_at": {
"date": "2019-05-16 04:13:12.000000",
"timezone_type": 3,
"timezone": "Europe\/London"
},
"updated_at": "2019-08-20T13:37:22.293428Z"
}.
Found: [
{
"id": 5,
"message": "King. 'When did you.",
"message_details": "<p>The Caterpillar.<\/p>",
"author": "hollis-dach",
"status": "pending",
"published_at": null,
"created_at": "2019-08-20 14:37:23",
"updated_at": "2019-08-20 14:37:23"
}
].
Here is my AnnouncementFactory
<?php
/* #var $factory \Illuminate\Database\Eloquent\Factory */
use Faker\Generator as Faker;
use App\User;
use App\Announcement;
use Carbon\Carbon;
$factory->define(Announcement::class, function (Faker $faker) {
$user = factory(User::class)->create();
return [
'message' => $faker->realText(25),
'message_details' => $faker->realText(20),
'author' => $user->username,
'status' => 'pending',
'published_at' => null,
'created_at' => $faker->dateTimeThisYear('now', 'Europe/London'),
'updated_at' => Carbon::now()
];
});
Here is my AnnouncementModel
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Purifier;
use Carbon\Carbon;
use App\Like;
class Announcement extends Model
{
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'message', 'message_details', 'status', 'published_at'
];
/**
* The attributes that should be mutated to dates.
*
* #var array
*/
protected $dates = [
'published_at',
'created_at',
'updated_at',
];
/**
* Get the user that posted this announcement
*/
public function user()
{
return $this->belongsTo(User::class, 'author', 'username');
}
/**
* Get the users that have liked this article
*
* #return void
*/
public function likes()
{
return $this->morphToMany(User::class, 'likeable');
}
/**
* Purify the content of message details when it is set so that it isn't vulnerable to XXS attacks
*
* #param string $value
* #return void
*/
public function setMessageDetailsAttribute($value)
{
$this->attributes['message_details'] = Purifier::clean($value);
}
/**
* Generate a nicer format for created_at
*
* #return void
*/
public function getCreatedAtAttribute($value)
{
return Carbon::parse($value)->format('d F Y');
}
/**
* Determine whether an announcement is pending
*
* #return void
*/
public function getPendingAttribute()
{
return $this->status == 'pending' ? true : false;
}
/**
* Check if the user has liked this announcement
*
* #return void
*/
public function getUserHasLikedAttribute()
{
$like = $this->likes()->whereUserId(auth()->user()->id)->first();
return (!is_null($like)) ? true : false;
}
/**
* Get the users that have liked this article
*
* #return void
*/
public function getLikesCountAttribute()
{
return $this->likes()->count();
}
/**
* Get count of users who liked the announcement excluding the logged in user
*
* #return void
*/
public function getLikesCountExcludingAuthUserAttribute()
{
return $this->likes()->where('username', '<>', auth()->user()->username)->count();
}
/**
* Get random user who liked this announcement
*
* #return void
*/
public function getRandomUserWhoLikedThisAttribute()
{
return $this->likes()->where('username', '<>', auth()->user()->username)->inRandomOrder()->first();
}
/**
* Get all users who liked this announcement
*
* #return void
*/
public function getUsersWhoLikedThisAttribute()
{
return $this->likes()->where('username', '<>', auth()->user()->username)->get();
}
/**
* Scope an article by whether or not it's published
*/
public function scopePublished($query)
{
return $query->where('status', 'published');
}
/**
* Scope an article by whether or not it's drafted
*/
public function scopePending($query)
{
return $query->where('status', 'pending');
}
/**
* Scope an article by whether or not it's archived
*/
public function scopeArchived($query)
{
return $query->where('status', 'archived');
}
}
Do the attributes literally have to be identical or am I just using tests incorrectly?
Do factories use model accessors and mutators?
I don't think you need to be testing the created_at and updated_at fields in this case. The reason why your test is failing is because the created_at value on the array is an instance of Carbon\Carbon rather than a string representing the datetime.
I would just update your factory to remove the created_at and updated_at values.
Try changing your factory to the following:
$factory->define(Announcement::class, function (Faker $faker) {
$user = factory(User::class)->create();
return [
'message' => $faker->realText(25),
'message_details' => $faker->realText(20),
'author' => $user->username,
'status' => 'pending',
'published_at' => null
];
});
Basically you are checking the database with the wrong information.
1 - announcements.store is an api which creates a new announcements for you.
2 - when you are calling assertDatabase, it's mean you have some information in your hand which you can check against the database. But if you take a look at the exception you can see you want to check this
"message": "King. 'When did you.",
"message_details": "The Caterpillar.",
"author": "beatrice-herzog",
"status": "pending",
"published_at": null,
"created_at": {
"date": "2019-05-16 04:13:12.000000",
"timezone_type": 3,
"timezone": "Europe\/London"
},
"updated_at": "2019-08-20T13:37:22.293428Z"
with this
"message": "King. 'When did you.",
"message_details": "<p>The Caterpillar.<\/p>",
"author": "hollis-dach",
"status": "pending",
"published_at": null,
"created_at": "2019-08-20 14:37:23",
"updated_at": "2019-08-20 14:37:23"
So either way you need to change the clause you are using. in this case you need to fix this three fields values(created_at, updated_at, message_details) or just remove them.
Yes, the key / value pairs you pass to assertDatabaseHas have to be identical to the record in your database. The function just searches for a row having the given column (key) with the specified value.
Be careful about converting your models to arrays. The resulting array may contain unexpected fields (relations) so better explicitly state the fields you want to check for.
I suggest you only include the properties of your announcement you want to test omitting the timestamps and other meta information.
Like this:
$this->assertDatabaseHas('announcements', Arr::only($announcement, [
'message', 'status'
]));
or like this:
$this->assertDatabaseHas('announcements', [
'message' => $announcement->message,
'status' => $announcement->status,
]);

Exclude null properties in JMS Serializer

My consumed XML API has an option to retrieve only parts of the response.
This causes the resulting object to have a lot of NULL properties if this feature is used.
Is there a way to actually skip NULL properties? I tried to implement an exclusion strategy with
shouldSkipProperty(PropertyMetadata $property, Context $context)`
but i realized there is no way to access the current property value.
An example would be the following class
class Hotel {
/**
* #Type("string")
*/
public $id;
/**
* #Type("integer")
*/
public $bookable;
/**
* #Type("string")
*/
public $name;
/**
* #Type("integer")
*/
public $type;
/**
* #Type("double")
*/
public $stars;
/**
* #Type("MssPhp\Schema\Response\Address")
*/
public $address;
/**
* #Type("integer")
*/
public $themes;
/**
* #Type("integer")
*/
public $features;
/**
* #Type("MssPhp\Schema\Response\Location")
*/
public $location;
/**
* #Type("MssPhp\Schema\Response\Pos")
*/
public $pos;
/**
* #Type("integer")
*/
public $price_engine;
/**
* #Type("string")
*/
public $language;
/**
* #Type("integer")
*/
public $price_from;
}
which deserializes in this specific api call to the following object with a lot of null properties.
"hotel": [
{
"id": "11230",
"bookable": 1,
"name": "Hotel Test",
"type": 1,
"stars": 3,
"address": null,
"themes": null,
"features": null,
"location": null,
"pos": null,
"price_engine": 0,
"language": "de",
"price_from": 56
}
]
But i want it to be
"hotel": [
{
"id": "11230",
"bookable": 1,
"name": "Hotel Test",
"type": 1,
"stars": 3,
"price_engine": 0,
"language": "de",
"price_from": 56
}
]
You can configure JMS Serializer to skip null properties like so:
$serializer = JMS\SerializerBuilder::create();
$serializedString = $serializer->serialize(
$data,
'xml',
JMS\SerializationContext::create()->setSerializeNull(true)
);
Taken from this issue.
UPDATE:
Unfortunately, if you don't want the empty properties when deserializing, there is no other way then removing them yourself.
However, I'm not sure what your use case for actually wanting to remove these properties is, but it doesn't look like the Hotel class contains much logic. In this case, I'm wondering whether the result has should be a class at all ?
I think it would be more natural to have the data represented as an associative array instead of an object. Of course, JMS Serializer cannot deserialize your data into an array, so you will need a data transfer object.
It's enough that you add dumpArray and loadArray methods to your existing Hotel class. These will be used for transforming the data into your desired result and vice versa. There is your DTO.
/**
* Sets the object's properties based on the passed array
*/
public function loadArray(array $data)
{
}
/**
* Returns an associative array based on the objects properties
*/
public function dumpArray()
{
// filter out the properties that are empty here
}
I believe it's the cleanest approach and it might reflect what you're trying to do more.
I hope this helps.

Symfony - Deserialize json to an array of entities

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.

Categories