FOSRestBundle: routes and annotations for parameters - symfony

I'm able to get GET parameters with #QueryParam() annotation, but it looks like it works for Query String data only: /user?id=123.
I'd prefer to have it like /user/123 instead. For this, I might use #Get("/user/{id}") annotation, but I don't see it has additional metadata which #QueryParam() has:
name="id", requirements="\d+", default="1", description="User id"
If I use both of the annotations, I get an error:
ParamFetcher parameter conflicts with a path parameter 'id' for route 'getone'
My conflicting docblock:
/**
* Finds and displays a Users entity.
*
* #Rest\View
* #Rest\Get("/user/{id}")
* #Rest\QueryParam(name="id", requirements="\d+", default="1", description="User id")
* #ApiDoc(section="Partner Users")
* #param int $id
* #return array
*/
PS I need to have an id in the path (/user/123), not in query, and I also need to use #QueryParam() as it's read by NelmioApiDocBundle. How may I resolve this issue?

FOSRestBundle's #Get annotation extends FOSRestBundle's #Route which in turn extends SensioFrameworkExtraBundle's #Route.
Have a look at the code and see the documentation chapter #Route and #Method.
The requirements and defaults attributes expect an array.
/**
* #Rest\View
* #Rest\Get("/user/{id}", requirements={"id" = "\d+"}, defaults={"id" = 1})
* #ApiDoc(
* description="Returns a User Object",
* parameters={
* {"name"="id", "dataType"="integer", "required"=true, "description"="User Id"}
* }
* )
*/
public function getAction($id)
{
// ...
}

If you want a description for requirements just do this in your annotation
/**
* #Rest\View
* #Rest\Get("/user/{id}", requirements={"id" = "\d+"}, defaults={"id" = 1})
* #ApiDoc(
* description="Returns a User Object",
* requirements={
* {"name"="id", "dataType"="integer", "required"=true, "description"="User Id"}
* }
* )
*/

Related

ApiPlatform - implement security authorization on subresource route

I'm using Symfony5 and ApiPlatform
I have a User entity and a Product entity.
I want to list all my user's products through a subressource, to do so I've implemented my user class as follow :
/**
* #ApiResource(
* attributes={
* "normalization_context"={"groups"={"user:read", "user:list"}},
* "denormalization_context"={"groups"={"user:put", "user:post"}}
* },
* subresourceOperations={
* "api_users_consultations_get_subresource"={
* "method"="GET",
* "security"="is_granted('ROLE_ADMIN')"
* }
* },
* collectionOperations={
* "get"={
* "method"="GET",
* "security"="is_granted('ROLE_ADMIN')",
* "normalization_context"={"groups"={"user:list"}}
* },
* "post"={
* "method"="POST",
* "security_post_denormalize"="is_granted('POST', object)",
* "denormalization_context"={"groups"={"user:post"}}
* }
* },
* itemOperations={
* "get"={
* "method"="GET",
* "security"="is_granted('GET', object)",
* "normalization_context"={"groups"={"user:read"}}
* }
* }
* )
* #Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false, hardDelete=false)
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
class User
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"user:read", "user:list"})
*
*/
private $id;
/**
* #ORM\OneToMany(targetEntity=Product::class, mappedBy="user")
* #ApiSubresource()
*/
private $product;
}
It does create a route /users/{id}/products and return me what I want.
The part where I'm blocking is when I want to add authorization to this route:
ROLE_ADMIN can access this route
ROLE_USER who own the ressource can access it
All the other roles will receive FORBIDDEN
To do so I've followed the doc : https://api-platform.com/docs/core/subresources/#using-serialization-groups
Added subresourceOperations to #ApiSubresource annotation
recovered the name of my grenerated route api_users_consultations_get_subresource through the bin/console debug:router command
and simply set a security=is_granted('ROLE_ADMIN') method like for other operations.
or security=is_granted('SUB_LIST', object) to hit the voter
but when I run my tests I get 200 where I should receive 403, the UserVoter or ProductVoter are not triggered nor the is_granted('ROLE_ADMIN') rule.
As if the subresourceOperations annotation wasn't recognize by the ApiPlatform.
I've also tried changing the name of the operation from api_users_consultations_get_subresource to :
consultations_get_subresource
api_consultations_get_subresource
clients_get_subresource
api_clients_get_subresource
and different other variants as I saw on Github it solved the issue in some cases (like here https://github.com/api-platform/api-platform/issues/1581#issuecomment-662503549) but it has not worked for me.
So I'm wondering is there something I havn't done to implement it correctly ?
Is it a known issue of ApiPlatform ?
Does anyone see where my logic is failing ?
Is there another way to setup security on subresource routes ?
Are there more docs on security realted to subresource? I have not find a lot of material on this particular subject

How can I change api-platform {id} to a different name?

I am using api-platform with Symfony 4. It works fine, but I would like to change a GET url from:
/booking/{id} to /booking/{bookingId}
I'm using my own DTO object and custom data provider (not Doctrine ORM).
Here are the current #ApiResource and #ApiProperty definitions that work fine:
/**
*
* #ApiResource(
* itemOperations={
* "get"={
* "path"="/booking/{id}",
* },
* "api_bookings_get_item"={
* "swagger_context"={
* "operationId"="getBookingItem",
* "summary"="Retrieves details on a booking",
* "parameters"= {
* {
* "name"="id",
* "description"="Booking ID",
* "default"="15000",
* "in"="path",
* "required"=true,
* "type"="string"
* }
* },
* "responses"={
* "200"={
* "description"="Results retrieved"
* },
* "404"={
* "description"="Booking not found"
* }
* }
* }
* }
* },
* collectionOperations={}
* )
*/
final class Booking
{
/**
* #var string
* #Assert\NotBlank
*
* #ApiProperty(
* identifier=true,
* attributes={
* "swagger_context"={
* "description"="Booking ID",
* "required"=true,
* "type"="string",
* "example"="123456"
* }
* }
* }
*/
public $id;
// other variables
}
However, if I change all the references from 'id' to 'bookingId' it stops working and I get a 404 error. Here are the changes I made to the above code:
"path"="/booking/{bookingId}"
"name"="bookingId"
public $bookingId;
Is api-platform hard-coded to use 'id' as an identifier? Is there any way to change this?
In Api-platform the id parameter is hardcoded:
namespace ApiPlatform\Core\DataProvider;
private function extractIdentifiers(array $parameters, array $attributes)
{
if (isset($attributes['item_operation_name'])) {
if (!isset($parameters['id'])) {
throw new InvalidIdentifierException('Parameter "id" not found');
}
but you can create your own operation and use the parameter name that you want there is a great example in docs custom operations
You can customize the apiResource identifier via:
/**
* #ApiProperty(identifier=false)
*/
private $id;
and:
/**
* #ApiProperty(identifier=true)
*/
private $uuid;
where uuid is the new identifier to be used in request URLs.
For reference: symfonycasts did an excellent tutorial here:
https://symfonycasts.com/screencast/api-platform-extending/uuid-identifier

how to make a property to be "merged" or initialized when deserializing with JMSSerializerBundle

I'm using JMSSerializer - along with the Doctrine constructor - in order to deserialize an object sent.
My (simplified) entities are the following. I omit the code I think is useless:
Widget
{
protected $id;
/**
* #ORM\OneToMany(
* targetEntity="Belka\Iso50k1Bundle\Entity\VarSelection",
* mappedBy="widget",
* cascade={"persist", "remove", "detach", "merge"})
* #Serializer\Groups({"o-all-getCWidget", "i-p2-create", "o-all-getWidget", "i-p3-create", "i-p2-editWidget"})
* #Type("ArrayCollection<Belka\Iso50k1Bundle\Entity\VarSelection>")
*/
protected $varsSelection;
}
/**
* #ORM\Entity()
*
* #ORM\InheritanceType("SINGLE_TABLE")
*
* #ORM\DiscriminatorColumn(
* name="vartype",
* type="string")
*
* #ORM\DiscriminatorMap({
* "PHY" = "PhyVarSelection"
* })
*
* #ORM\HasLifecycleCallbacks()
*/
abstract class VarSelection
{
/**
* #Id
* #Column(type="integer")
* #GeneratedValue("SEQUENCE")
* #Serializer\groups({"o-all-getCWidget", "o-all-getWidget", "i-p2-editWidget"})
*/
protected $id;
}
class PhyVarSelection extends VarSelection
{
/**
* #var PhyVar
*
* #ORM\ManyToOne(
* targetEntity="Belka\Iso50k1Bundle\Entity\PhyVar",
* cascade={"persist", "merge", "detach"})
*
* #ORM\JoinColumn(
* name="phy_var_sel",
* referencedColumnName="id",
* nullable=false)
*/
protected $phyVar;
}
class PhyVar extends Variable
{
/**
* #ORM\Column(type="string")
* #ORM\Id
*
* #Serializer\Groups({"o-p2-getCMeters", "o-all-getWidget"})
* #Assert\Regex("/(PHY)_\d+_\d+_\w+/")
*/
protected $id;
/**
* #ORM\Column(type="text", name="varname")
* #Serializer\Groups({"o-p2-getCMeters", "o-all-getWidget", "o-all-getCWidget"})
*/
protected $varName;
...
}
I try to deserialize an object that represents a Widget entity already persisted, along with which an array of varselection with their own id specified - if already persisted - and without their own id if they are new and to be persisted.
Deserialization works:
$context = new DeserializationContext();
$context->setGroups('i-p2-editWidget');
$data = $this->serializer->deserialize($content, $FQCN, 'json', $context);
but $data has always Widget::$varsSelection[]::$phyVar as a proxy class initialized, with only the id properly set. What I have to do so as to have it all is:
foreach ($data->getVarsSelection() as $varSel) {
$varSel->getVar();
}
why is that? How can have it initialized already? I don't want to spend time cycling and fetching data from DB again.
edit
I've added a domain of the entities so as to get the idea of what I'm deserializing
I figured out myself the hows and whys of this behavior:
since I'm sending a JSON like the following:
{
"id": <widgetID>,
"vars_selection": {
"id": <varSelectionID>,
"vartype": "PHY"
}
}
JMSSerializer's Doctrine ObjectConstructor simply tries to finds just two Entities: Widget and VarSelection by executing the following line:
$object = $objectManager->find($metadata->name, $identifierList);
in other words: Doctrine's EntityManager tries to find the Entity identified by its ID. Hence, well'get the unitialized proxy classes.
As far as I know, find cannot specify an hydration mode. Hence, two are the ways to handle this:
Specify fetch="EAGER" on PhyVarSelection::$phyVar. Quite costly, when we do not need it though;
Replace the ObjectConstructor by calling the repository and make a DQL, which will have the EAGER option properly set. Something like $query->setFetchMode("PhyVarSelection", "phyVar", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER);

how to deserialize a POST body content into an Entity with #ExclusionPolicy("all") set

I'm using JMSSerializerBundle and FOSRestBundle and I'm trying to deserialize my body request by means of the #ParamConverter annotation:
/**
* #View()
*
* #Route("/users/{username}/globaltoken", defaults={"_format" = "json"}, requirements={"user"="\w+"})
* #ParamConverter(
* "userBody", class="Belka\AuthBundle\Entity\User",
* converter="fos_rest.request_body"
* )
*/
public function postAction($username, User $userBody)
{
...
The User entity has #ExclusionPolicy("all") set and some attributes are #exposed. That's perfect when I serialize; unfortunatly, when it comes to deserializing my body into a User object the unexposed attribtues are not set. Is there a clean way to handle this?
Answering myself: #ExclusionPolicy(“all”) is not what you want for security purposes. That tag was born for handling data that should not be serialized, whether or not sometimes it should not appear for security reasons. It's a static thing and it's OK like that.
What I really wanted is managing what to show or not (or consider for deserialization) by using groups. Hence:
Declare some groups and assign on the attributes
Use the desired groups in the controller: the deserialization and serialization will consider only the attributes belonging to at least one group declared.
An example:
* Entity *
class User implements EncoderAwareInterface
{
/**
* #ORM\Id
* #ORM\Column(type="string")
* #Assert\NotBlank(message = "user.username.not_blank")
* #ORM\GeneratedValue(strategy="NONE")
* #Serializer\Groups({"default"})
*/
private $username;
/**
* #ORM\Column(type="string", nullable=true)
* #Serializer\Groups("personal")
*/
private $password;
...
* Controller *
/**
* #ParamConverter(
* "userBody",
* class="Belka\AuthBundle\Entity\User",
* converter="fos_rest.request_body",
* options={"deserializationContext"={"groups"={"personal"}}}
* )
*/
public function postAction($username, User $userBody, $_format)
{
that way, only password will be deserialized.

Annotation to show Entity's property description - using nelmioapidocbundle

I am using nelmioapidocbundle in order to documents my Rest API built on the top of symfony-2.x.
I can't found the right annotation to use to show each Entity's property description on the return section (Please see bellow attached image).
My Entity :
/**
* Checkins
*
* #ORM\Table(name="CheckIns")
* #ORM\Entity(repositoryClass="Project1\ApiBundle\Entity\CheckinsRepository")
*
* #ExclusionPolicy("none")
*/
class Checkins
{
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
* #Groups({"checkin"})
* #
*/
private $id;
My Controller :
class CheckinController extends BaseRestController
{
/**
* #ApiDoc(
* resource=true,
* description="Find checkin by ID",
*
* parameters={
* {"name"="categoryId", "dataType"="integer", "required"=true, "description"="checkin id"}
* }
*
* output={
* "class"="Project1\ApiBundle\Entity\Checkins",
* "groups"={"checkin"}
* },
* statusCodes={
* 200="Checkin found",
* 400="ID is required",
* 404="Checkin not found"
* }
* )
*
* #Rest\View()
*/
public function getAction(Request $request)
{}
Result ( Description column is empty ) :
There's a description in doc section of the bundle:
For classes parsed with JMS metadata, description will be taken from the properties doc comment, if available.
For Form Types, you can add an extra option named description on each
field
Please visit the following link for more instructions(info at the bottom of the section):
https://github.com/nelmio/NelmioApiDocBundle/blob/master/Resources/doc/index.md#the-apidoc-annotation

Resources