FOSRestBundle: partial response in function of attributes asked in the request
Asked Answered
N

2

6

Context

I found a lot of questions about partial API response with FOSRest and all the answers are based on the JMS serializer options (exlude, include, groups, etc). It works fine but I try to achieve something less "static".

Let's say I have a user with the following attributes: id username firstname lastname age sex

I retrieve this user with the endpoint GET /users/{id} and the following method:

/**
 * @View
 *
 * GET /users/{id}
 * @param integer $user (uses ParamConverter)
 */
public function getUserAction(User $user) {
    return $user;
}

The method returns the user with all his attributes.

Now I want to allow something like that: GET /users/{id}?attributes=id,username,sex

Question

Did I missed a functionality of FOSRestBUndle, JMSserializer or SensioFrameworkExtraBundle to achieve it automatically? An annotation, a method, a keyword in the request or something else?

Otherwise, what is the best way to achieve it?

Code

I thought to do something like that:

/**
 * @View
 * @QueryParam(name="attributes")
 *
 * GET /users/{id}
 *
 * @param integer $user (uses ParamConverter)
 */
public function getUserAction(User $user, $attributes) {
    $groups = $attributes ? explode(",", $attributes) : array("Default");

    $view = $this->view($user, 200)
        ->setSerializationContext(SerializationContext::create()->setGroups($groups));

    return $this->handleView($view);
}

And create a group for each attribute:

use JMS\Serializer\Annotation\Groups;

class User {

    /** @Groups({"id"}) */
    protected $id;

    /** @Groups({"username"}) */
    protected $username;

    /** @Groups({"firstname"}) */
    protected $firstname;

    //etc
}
Nannettenanni answered 11/12, 2014 at 11:5 Comment(0)
H
3

You can do it like that through groups, as you've shown. Maybe a bit more elegant solution would be to implement your own ExclusionStrategy. @Groups and other are implementations of ExclusionStrategyInterface too.

So, say you called your strategy SelectFieldsStrategy. Once you implement it, you can add it to your serialization context very easy:

$context = new SerializationContext();
$context->addExclusionStrategy(new SelectFieldsStrategy(['id', 'name', 'someotherfield']));
Husserl answered 11/12, 2014 at 13:14 Comment(2)
Do you know if there is a way to put all this logic in an annotation (directly inside @View or inside a new annotation)? Actually I need fields exclusion everywhere and the code will strictly be identical in each endpoint. I'm pretty new to php and symfony so if you have some links, I will appreciate it. :)Nannettenanni
It's definitely possible but I have never made custom annotation so I can't really help you with implementation guidelines, you'll have to research that part yourself :)Transilluminate
N
4

My implementation based on Igor's answer:

ExlusionStrategy:

use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;

class FieldsExclusionStrategy implements ExclusionStrategyInterface {
    private $fields = array();

    public function __construct(array $fields) {
        $this->fields = $fields;
    }

    public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext) {
        return false;
    }

    public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext) {
        if (empty($this->fields)) {
            return false;
        }

        if (in_array($property->name, $this->fields)) {
            return false;
        }

        return true;
    }
}

Controller:

/**
 * @View
 * @QueryParam(name="fields")
 *
 * GET /users/{id}
 *
 * @param integer $user (uses ParamConverter)
 */
public function getUserAction(User $user, $fields) {
    $context = new SerializationContext();
    $context->addExclusionStrategy(new FieldsExclusionStrategy($fields ? explode(',', $fields) : array()));

    return $this->handleView($this->view($user)->setSerializationContext($context));
}

Endpoint:

GET /users/{id}?fields=id,username,sex
Nannettenanni answered 11/12, 2014 at 14:42 Comment(2)
Any ideas on how to handle related entities with this approach? In the PropertyMetadata there doesn't seem to be any information that could indicate that given field is an object.Effortful
I figured it out: $context->getDepth() could be used for this. In my case I'm checking if it's more than 1.Effortful
H
3

You can do it like that through groups, as you've shown. Maybe a bit more elegant solution would be to implement your own ExclusionStrategy. @Groups and other are implementations of ExclusionStrategyInterface too.

So, say you called your strategy SelectFieldsStrategy. Once you implement it, you can add it to your serialization context very easy:

$context = new SerializationContext();
$context->addExclusionStrategy(new SelectFieldsStrategy(['id', 'name', 'someotherfield']));
Husserl answered 11/12, 2014 at 13:14 Comment(2)
Do you know if there is a way to put all this logic in an annotation (directly inside @View or inside a new annotation)? Actually I need fields exclusion everywhere and the code will strictly be identical in each endpoint. I'm pretty new to php and symfony so if you have some links, I will appreciate it. :)Nannettenanni
It's definitely possible but I have never made custom annotation so I can't really help you with implementation guidelines, you'll have to research that part yourself :)Transilluminate

© 2022 - 2024 — McMap. All rights reserved.