How to prevent lazy loading of vulnarable entities in Symfony - php

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.

Related

NelmioApiDocBundle annotations for nested objects

I am trying to add annotations for a model which has references of child nodes. But those child nodes are being called strings. The same code works just fine if it is referencing a different class. Is there any way this can be accomplished?
class Foo
{
private int $id;
/**
* #OA\Property(
* type="array",
* #OA\Items(
* ref=#Model(type=Foo::class),
* )
* ),
* #var array<Foo>
*/
private array $children;
}
Result
{
"id": 0,
"children": "string"
}
Expected
{
"id": 0,
"children": [{
"id": 0,
"children": [{}]
}]
}

Only return specific fields for specific groups when serializing with Symfony 4

Symfony 4. I have two entities, Cat and Owner.
class Cat
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
* #Groups("cats")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Groups("cats")
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Owner", mappedBy="cat")
* #Groups("cats")
*/
private $owners;
}
class Owner
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
* #Groups("cats")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Groups("owners")
*/
private $name;
}
My API endpoint needs to return 2 keys, owners (a list of all owners), and cats (a list of all cats, with their owners).
public function index()
{
$repository = $this->getDoctrine()->getRepository(Owner::class);
$owners = $repository->findAll();
$repository = $this->getDoctrine()->getRepository(Cat::class);
$cats = $repository->findAll();
return $this->json([
'owners' => $owners,
'cats' => $cats,
], 200, [], ['groups' => ['owners', 'cats']]);
}
This works, but with 1 problem: the cats list contains the full owner information for each owner, i.e.:
{
"owners": [
{
"id": 1,
"name": "John Smith"
}
],
"cats": [
{
"id": 1,
"name": "Miaow",
"owners": [
{
"id": 1,
"name": "John Smith"
}
]
}
]
}
What I want is for the owners key in the cat object to only return the owner's id, like this:
{
"owners": [
{
"id": 1,
"name": "John Smith"
}
],
"cats": [
{
"id": 1,
"name": "Miaow",
"owners": [
1
]
}
]
}
You can use a getter for a specific group and with a specific serialized name.
In Cat:
/**
* #Groups("cats")
* #SerializedName("owners")
*/
public function getOwnersIds(): iterable
{
return $this->getOwners()->map(function ($owner) {
return $owner->getId();
})->getValues();
}
Override the mapping of a field.
See doctrine documentation:
https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/inheritance-mapping.html

Doctrine - how to get scalars hydrated to the related children level

i'm trying to get from the database some Tasks with the count of contents (related to task) and the assignment objects (related to task) with the count of answers (related to assignment).
So I'm using scalars for the counts and it's pretty fine but I cannot manage to have one count_of_answers by assignments...
Here if my entities relationships :
class Task
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\Assignment", mappedBy="task", orphanRemoval=true)
*/
private $assignments;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Content", mappedBy="task", orphanRemoval=true)
*/
private $contents;
}
class Content
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Task", inversedBy="contents")
* #ORM\JoinColumn(nullable=false)
*/
private $task;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Answer", mappedBy="content", orphanRemoval=true)
*/
private $answers;
}
class Assignment
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Task", inversedBy="assignments")
* #ORM\JoinColumn(nullable=false)
*/
private $task;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Answer", mappedBy="assignment", orphanRemoval=true)
*/
private $answers;
}
class Answer
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Content", inversedBy="answers")
* #ORM\JoinColumn(nullable=false)
*/
private $content;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Assignment", inversedBy="answers")
* #ORM\JoinColumn(nullable=false)
*/
private $assignment;
}
I would like to get in one query (I think this is possible to avoid querying inside a loop) something like :
Array [[
Task1{
properties,
assignments [
Assignment1{
properties,
count_of_answers
},
Assignment2{
properties,
count_of_answers
},...
]
},
count_of_contents
],
[
Task2{},
count_of_contents
],...
]
So i tried this query in my task repository :
$r = $this->createQueryBuilder('t')
->innerJoin('t.assignments', 'a')
->addSelect('a')
->innerJoin('t.contents', 'c')
->addSelect('COUNT(DISTINCT c.id) AS count_of_contents')
->leftJoin('a.answers', 'an')
->addSelect('COUNT(DISTINCT an.id) AS count_of_answers')
->groupBy('a.id')
->getQuery()
->getResult();
But it's giving something like :
Array[[
Task1{}
count_of_contents,
count_of_answers
],
Task2{}
count_of_contents,
count_of_answers
]]
Could you please help ?
Maybe I should use DQL with a subquery but I'm affraid to lose performence in the sql side. I believe the data fetched are good (when I try the sql query, I do have one count_of_answers by assignment), but the hydratation is not correctly mapped and I only get the last count_of_answers associated to the task instead of assignment.

Serializer using Normalizer returns nothing when using setCircularReferenceHandler

Question:
Why does my response return "blank" when I set the setCircularReferenceHandler callback?
EDIT:
Would appear that it returns nothing, but does set the header to 500 Internal Server Error. This is confusing as Symfony should send some kind of error response concerning the error?
I wrapped $json = $serializer->serialize($data, 'json'); in a try/catch but no explicit error is thrown so nothing is caught. Any ideas would be really helpful.
Context:
When querying for an Entity Media I get a blank response. Entity Media is mapped (with Doctrine) to Entity Author. As they are linked, indefinite loops can occur when trying to serialize.
I had hoped that using the Circular Reference Handler I could avoid just that, but it's not working.
Error:
This is the error I'm getting when I'm NOT setting the Circular Reference Handler:
A circular reference has been detected when serializing the object of class "Proxies__CG__\AppBundle\Entity\Author\Author" (configured limit: 1) (500 Internal Server Error)
Now this error is completely expected, as my Entity Author points back to the Entity Media when originally querying for a Media ( Media -> Author -> Media )
class Author implements JsonSerializable {
//Properties, Getters and setters here
/**
* Specify data which should be serialized to JSON
* #link http://php.net/manual/en/jsonserializable.jsonserialize.php
* #return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* #since 5.4.0
*/
function jsonSerialize()
{
return [
"title" => $this->getTitle(),
"id" => $this->getId(),
"firstname" => $this->getFirstname(),
"lastname" => $this->getLastname(),
//This is the problem right here. Circular reference.
"medias" => $this->getAuthorsMedia()->map(function($object){
return $object->getMedia();
})
];
}
}
What I've tried:
My Entities implement JsonSerializable interface so I define what attributes are returned (Which is what JsonSerializeNormalizer requires). This works completely when I remove the "medias" property in the Author's class, everything works.
Here is how I use my serliazer with my normalizer.
/**
* #Route("/media")
* Class MediaController
* #package BackBundle\Controller\Media
*/
class MediaController extends Controller
{
/**
* #Route("")
* #Method({"GET"})
*/
public function listAction(){
/** #var MediaService $mediaS */
$mediaS= $this->get("app.media");
/** #var array $data */
$data = $mediaS->getAll();
$normalizer = new JsonSerializableNormalizer();
$normalizer->setCircularReferenceLimit(1);
$normalizer->setCircularReferenceHandler(function($object){
return $object->getId();
});
$serializer = new Serializer([$normalizer], [new JsonEncoder()]);
$json = $serializer->serialize($data, 'json');
return new Response($json);
}
}
Github issue opened
I tried to reproduce your error, and for me everything worked as expected (see code samples below).
So, your setCircularReferenceHandler() works fine.
Maybe try to run my code, and update it with your real entities and data sources step by step, until you see what causes the error.
Test (instead of your controller):
class SerializerTest extends \PHPUnit\Framework\TestCase
{
public function testIndex()
{
$media = new Media();
$author = new Author();
$media->setAuthor($author);
$author->addMedia($media);
$data = [$media];
$normalizer = new JsonSerializableNormalizer();
$normalizer->setCircularReferenceLimit(1);
$normalizer->setCircularReferenceHandler(function($object){
/** #var Media $object */
return $object->getId();
});
$serializer = new Serializer([$normalizer], [new JsonEncoder()]);
$json = $serializer->serialize($data, 'json');
$this->assertJson($json);
$this->assertCount(1, json_decode($json));
}
}
Media entity
class Media implements \JsonSerializable
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var Author
*
* #ORM\ManyToOne(targetEntity="Author", inversedBy="medias")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
private $author;
/**
* {#inheritdoc}
*/
function jsonSerialize()
{
return [
"id" => $this->getId(),
"author" => $this->getAuthor(),
];
}
//todo: here getter and setters, generated by doctrine
}
Author entity
class Author implements \JsonSerializable
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var Media[]
*
* #ORM\OneToMany(targetEntity="Media", mappedBy="author")
*/
private $medias;
/**
* {#inheritdoc}
*/
function jsonSerialize()
{
return [
"id" => $this->getId(),
"medias" => $this->getMedias(),
];
}
//todo: here getter and setters, generated by doctrine
}

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.

Categories