5

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.

Community
  • 1
  • 1
James Crinkley
  • 1,398
  • 1
  • 13
  • 33

1 Answers1

1

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.

chalasr
  • 12,971
  • 4
  • 40
  • 82
  • Yes this makes sense! Does this means that, assuming I wanted to apply the same principle to individual fields (non-relationships such as the `title` in your example) I'd need groups for each field? I'm guessing that this is the only way to achieve this, it just seems a little weird that you can't define your relationships when building a Doctrine QueryBuilder instance (conditionally adding joins) and have the serializer just serialize that data, without. Thanks for the reply though, I'll accept this answer for now :) – James Crinkley Mar 25 '16 at 22:21
  • Also, sorry to tack on additional questions, but do you know how you would handle nested fields? (i.e. you're retrieving posts, but you want to include/exclude say a `User` relationship within a `Foo` entity above)? – James Crinkley Mar 25 '16 at 22:23
  • 2
    See [the `@MaxDepth` annotation](http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#limiting-serialization-depth-of-some-properties) to limit the depth of the associations . If you need to exclude/expose totally some fields, you will surely need to deal with Groups for this too. If you really want use a custom QueryBuilder, don't use the serializer at all, and hydrate your result as array (i.e. `$qb->getQuery->getArrayResult()`), you should get only the fields added by a select statement. – chalasr Mar 26 '16 at 10:51
  • 1
    I.. I don't know know why I never ever even considered the array hydration mode. Thank you! – James Crinkley Mar 26 '16 at 12:32