Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Advice for implementing field whitelists with Symfony/FosRestBundle/JMS Serializer

I'm currently learning how to implement a relatively simple API using Symfony 3 (with FOSRestBundle) and JMS Serializer. I've been trying recently to implement the ability to specify, as a consuming client, which fields should be returned within a response (both fields within the requested entity and relationships). For example;

  • /posts with no include query string would return all Post entity properties (e.g. title, body, posted_at etc) but no relationships.
  • /posts?fields[]=id&fields[]=title would return only the id and title for posts (but again, no relationships)
  • /posts?include[]=comment would include the above but with the Comment relationship (and all of its properties)
  • /posts?include[]=comment&include[]=comment.author would return as above, but also include the author within each comment

Is this a sane thing to try and implement? I've been doing quite a lot of research on this recently and I can't see I can 1) restrict the retrieval of individual fields and 2) only return related entities if they have been explicitly asked for.

I have had some initial plays with this concept, however even when ensuring that my repository only returns the Post entity (i.e. no comments), JMS Serializer seems to trigger the lazy loading of all related entities and I can't seem to stop this. I have seen a few links such as this example however the fixes don't seem to work (for example in that link, the commented out $object->__load() call is never reached anyway in the original code.

I have implemented a relationship-based example of this using JMSSerializer's Group functionality but it feels weird having to do this, when I would ideally be able to build up a Doctrine Querybuilder instance, dynamically adding andWhere() calls and have the serializer just return that exact data without loading in relationships.

I apologise for rambling with this but I've been stuck with this for some time, and I'd appreciate any input! Thank you.

like image 973
James Crinkley Avatar asked Oct 31 '22 06:10

James Crinkley


1 Answers

You should be able to achieve what you want with the Groups exclusion strategy.

For example, your Post entity could look like this:

use JMS\Serializer\Annotation as JMS;

/**
 * @JMS\ExclusionPolicy("all")
 */
class Post
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(type="integer")
     *
     * @JMS\Expose
     * @JMS\Groups({"all", "withFooAssociation", "withoutAssociations"})   
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     *
     * @JMS\Expose
     * @JMS\Groups({"all", "withFooAssociation", "withoutAssociations"})
     */
    private $title;

    /**
     * @JMS\Expose
     * @JMS\Groups({"all", "withFooAssociation"})
     *
     * @ORM\OneToMany(targetEntity="Foo", mappedBy="post")
     */
    private $foos;
}

Like this, if your controller action returns a View using serializerGroups={"all"}, the Response will contains all fields of your entity.

If it uses serializerGroups={"withFooAssociation"}, the response will contains the foos[] association entries and their exposed fields.

And, if it uses serializerGroups={"withoutAssociation"}, the foos association will be excluded by the serializer, and so it will not be rendered.

To exclude properties from the target entity of the association (Fooentity), use the same Groups on the target entity properties in order to get a chained serialisation strategy.

When your serialization structure is good, you can dynamically set the serializerGroups in your controller, in order to use different groups depending on the include and fields params (i.e. /posts?fields[]=id&fields[]=title). Example:

// PostController::getAction

use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerBuilder;

$serializer = SerializerBuilder::create()->build();
$context = SerializationContext::create();
$groups = [];

// Assuming $request contains the "fields" param
$fields = $request->query->get('fields');

// Do this kind of check for all fields in $fields
if (in_array('foos', $fields)) {
    $groups[] = 'withFooAssociation';
}

// Tell the serializer to use the groups previously defined
$context->setGroups($groups);

// Serialize the data
$data = $serializer->serialize($posts, 'json', $context);

// Create the view
$view = View::create()->setData($data);

return $this->handleView($view);

I hope that I correctly understood your question and that this will be sufficient for help you.

like image 153
chalasr Avatar answered Nov 11 '22 09:11

chalasr