Custom Symfony Action with API Platform bundle - php

I try to build an API with the Symfony bundle API-Platform.
Api resource offer automatic CRUD action with the HTTP verbs POST, GET, PUT, DELETE.
What I want is adding an endpoint to handle a custom POST action, with a custom payload/body, not depending on any resource.
Where I block it is to add this endpoint to the automatic API-Platform documentation.
When looking for this kind of issue on GitHub, I found that the API-Platform v2 should be able to do it.
See Issue #385 : Custom action + ApiDoc
It looks like some people find the way to use NelmioApiDoc #ApiDoc annotation.
See Issue #17 : Documentation for custom operation

Using the #ApiDoc annotation is a no go, support for NelmioApiDoc will be removed in API Platform 3 in favor of the builtin Swagger/Hydra support.
If you use a custom API Platform action, the action should automatically be documented in Swagger and Hydra docs.
Anyway, you can always customize the Swagger (and Hydra) docs to add custom endpoints or anything else: https://github.com/api-platform/docs/blob/master/core/swagger.md#override-swagger-documentation (this documentation will be available on the website soon).

You can document your own route with the #ApiResource() annotation:
/**
* #ORM\Entity
* #ApiResource(
* itemOperations={
* "get"={"method"="GET"},
* "put"={"method"="PUT"},
* "delete"={"method"="DELETE"},
* "send_reset_password_token"={
* "route_name"="user_send_reset_password_token",
* "swagger_context" = {
* "parameters" = {
* {
* "name" = "email",
* "in" = "path",
* "required" = "true",
* "type" = "string"
* }
* },
* "responses" = {
* "201" = {
* "description" = "email with reset token has been sent",
* "schema" = {
* "type" = "object",
* "required" = {
* "email"
* },
* "properties" = {
* "email" = {
* "type" = "string"
* },
* "fullname" = {
* "type" = "string"
* }
* }
* }
* },
* "400" = {
* "description" = "Invalid input"
* },
* "404" = {
* "description" = "resource not found"
* }
* },
* "summary" = "Send email with token to reset password",
* "consumes" = {
* "application/json",
* "text/html",
* },
* "produces" = {
* "application/json"
* }
* }
* }
* },
* attributes={
* "normalization_context"={"groups"={"user", "user-read"}},
* "denormalization_context"={"groups"={"user", "user-write"}}
* }
* )
*/
Source: https://github.com/api-platform/docs/issues/143#issuecomment-260221717

You can create custom post action like this.
Map resources configuration to yaml.
# config/packages/api_platform.yaml
api_platform:
enable_swagger_ui: false
mapping:
paths: ['%kernel.project_dir%/config/api_platform']
Create resources.yaml
# config/api_platform/resources.yaml
resources:
App\Entity\User:
itemOperations: []
collectionOperations:
post:
method: 'POST'
path: '/auth'
controller: App\Controller\AuthController
swagger_context:
summary: your desc
description: your desc
Then in App\Entity\User add public properties
class User {
public $login
public $password
}
It is all, now in swagger ui u will see method POST /api/auth with login and pass parameters.
In u controller override _invoke for execute your logic.
class AuthController {
public function __invoke()
{
return ['your custom answer'];
}
}

I ran into the same situation because I tried to put the POST method into itemOperations, although it can only reside in collectionOperations. In the latter in can successfully define my custom path.
/**
* #ApiResource(
* collectionOperations={
* "get"={
* "path"="/name_your_route",
* },
* "post"={
* "path"="/name_your_route",
* },
* },
* itemOperations={
* "get"={
* "path"="/name_your_route/group/{groupId}/user/{userId}",
* "requirements"={"groupId"="\d+", "userId"="\d+"},
* },
* "delete"={
* "path"="/name_your_route/group/{groupId}/user/{userId}",
* },
* "put"={
* "path"="/name_your_route/group/{groupId}/user/{userId}",
* }
* })
Hopefully helpful for others that stumble upon this question.
And here is the paragraph from the great documentation about it:
Collection operations act on a collection of resources. By default two
routes are implemented: POST and GET. Item operations act on an
individual resource. 3 default routes are defined GET, PUT and DELETE

Related

Create a custom route for a collection using GET method

I do not manage to create a custom route for a collection, my entity is named File.
here is my Entity annotation :
/**
* #ApiResource(
*
* normalizationContext={"groups"={"file"},"enable_max_depth"=true},
* denormalizationContext={"groups"={"file-write-customers"},"enable_max_depth"=true},
* attributes={"force_eager"=false},
* itemOperations={
* "get",
* "put",
* "get_mandate_pdf"={
* "method"="POST",
* "path"="/files/{id}/mandate-pdf",
* "controller"=FileCreatePdfController::class,
* },
* },
* collectionOperations={
* "stats"={
* "method"="GET",
* "path"="/files/stats",
* "controller"=FileStatsController::class,
* }
* },
* )
* #ApiFilter(SearchFilter::class, properties={"status": "exact", "sponsor": "exact"})
* #ApiFilter(DateFilter::class, properties={"updatedAt"})
* #ORM\Entity
* #ORM\Table(name="cases")
*/
The controller file
<?php
namespace App\Controller;
use App\Entity\File;
class FileStatsController
{
public function __invoke(File $data): File
{
return $data;
}
}
however i have this error when i reach /files/stats, it seems that api plaform is expecting an Id .
For some reasons if i switch the method from GET to POST the route is working
{
"#context": "\/contexts\/Error",
"#type": "hydra:Error",
"hydra:title": "An error occurred",
"hydra:description": "The identifier id is missing for a query of App\\Entity\\File",
"trace": [
{
"namespace": "",
"short_class": "",
"class": "",
"type": "",
"function": "",
"file": "\/srv\/api\/vendor\/doctrine\/orm\/lib\/Doctrine\/ORM\/ORMException.php",
"line": 309,
"args": []
},
I manage to find the solution , in my Controller if i remove the typed variable $data
namespace App\Controller;
class FileStatsController
{
public function __invoke($data)
{
return $data;
}
}
I manage to properly retreive the data
in addition, the method get is mandatory in the ApiRessource annotations
collectionOperations={
"get",
* "stats"={
* "method"="GET",
* "path"="/stats",
* "controller"=FileStatsController::class,
* }
* }

Using Symfony voters on collectionOperations (GET)

I want to use symfony voters in API PLATFORM. I don't have any problem when I use it on itempsOperations (GET, PUT, DELETE), but when I use it in collectionOperations especially in GET (POST works well), I cannot access to the $subject because in GET operation API PLATFORM returns an instance of "ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator" and not entity object.
* #ApiResource(
* collectionOperations={
* "GET"={
* "access_control"="is_granted('GET', object)",
* },
* "POST"={
* "access_control"="is_granted('ADD', object)",
* }
* }
* )
How can I fix this?
I experienced the same problem, dont know if this is a feature or a bug. Since we're basically asking for a set of this recources. And with that in mind a pagination object would make sense, I guess.
A solution arround this issue could be the following:
#\Entity\YourEntity.php
* #ApiResource(
* collectionOperations={
* "GET"={
* "access_control"="is_granted('GET', _api_resource_class)",
* },
* }
* )
#\Security\Voter\YourVoter.php
/**
* Determines if the attribute and subject are supported by this voter.
*
* #param string $attribute An attribute
* #param mixed $subject The subject to secure, e.g. an object the user wants to access or any other PHP type
*
* #return bool True if the attribute and subject are supported, false otherwise
*/
protected function supports($attribute, $subject)
{
// If the subject is a string check if class exists to support collectionOperations
if(is_string($subject) && class_exists($subject)) {
$subject = new $subject;
}
if(in_array($attribute, ['GET'])
&& $subject instanceof YourEntity) {
return true;
}

api-platform | Is there a way to disable the WriteListener on a specific operation or route?

So because of api-platform.com Unable to generate an IRI for the item of type I tried using a different approach and declare custom operations on my user entity for login, registration and reset (since I stil want custom business logics for them). So the initial set-up of that in api-platform is rather easy. I added the following code to my user entity
* collectionOperations={
* "register"={"route_name"="user_register","normalization_context"={"groups"={"registerRead"}},"denormalization_context"={"groups"={"registerWrite"}}},
* "reset"={"route_name"="user_reset","normalization_context"={"groups"={"resetRead"}},"denormalization_context"={"groups"={"resetWrite"}}},
* "login"={"route_name"="user_login","normalization_context"={"groups"={"loginRead"}},"denormalization_context"={"groups"={"loginWrite"}}},
* "token"={"route_name"="user_token","normalization_context"={"groups"={"tokenRead"}},"denormalization_context"={"groups"={"token"}}}
* },
And then added the appropriate actions to the user controller.
/**
* #Route(
* name="user_login",
* path="api/user/login",
* methods={"POST"},
* defaults={
* "_api_resource_class"=User::class,
* "_api_collection_operation_name"="login",
* "_api_receive"=false
* }
* )
*/
public function loginAction(User $data): User {
///$this->userService->login($data);
return $data;
}
/**
* #Route(
* name="user_register",
* path="api/user/register",
* methods={"POST"},
* defaults={
* "_api_resource_class"=User::class,
* "_api_collection_operation_name"="register",
* "_api_receive"=false
* }
* )
*/
public function registerAction(User $data): User {
///$this->userService->register($data);
return $data;
}
/**
* #Route(
* name="user_reset",
* path="api/user/reset",
* methods={"POST"},
* defaults={
* "_api_resource_class"=User::class,
* "_api_collection_operation_name"="reset",
* "_api_receive"=false
* }
* )
*/
public function resetAction(User $data): User {
//$this->userService->reset($data);
return $data;
}
/**
* #Route(
* name="user_token",
* path="api/user/token",
* methods={"POST"},
* defaults={
* "_api_resource_class"=User::class,
* "_api_collection_operation_name"="token",
* "_api_receive"=false
* }
* )
*/
public function tokenAction(User $data): User {
//$this->userService->reset($data);
return $data;
}
So far al fine, however..... because we are using a post operation here and the user is a doctrine ORM entity the api-platform bundle atomically adds the post to the database. But I don’t want that, I want it to pass the entity on to the controller who then uses a service to do business logics. And determine if and how the post should be processed.
Now I went over the documentation and the problem seems to be that the WriteListener always triggers there were other triggers (e.g. ReadListener, DeserializeListener and ValidateListener) can be disabled trough the _api_receive parameter.
So that leaves the question is there a way to disable the WriteListener on a specific operation or route?
Kind Regards,
Ruben van der Linde
You can return an instance of HttpFoundation's Response instead of $data. Then no listener registered on kernel.view will be called.
But introducing a listener similar to api_receive for the write listener is a good idea. Would you mind opening a Pull Request?
Edit: I've opened a Pull Request to introduce this new flag: https://github.com/api-platform/core/pull/2072

Drupal 8.3 Custom Rest POST Error BadRequestHttpException: The type link relation must be specified

I have try to create a Custom REST POST plugin in my Drupal 8.3.2 for get an external JSON and then create an article from that.
I have follow that guide: How to create Custom Rest Resources for POST methods in Drupal 8
And this is my code:
<?php
namespace Drupal\import_json_test\Plugin\rest\resource;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\node\Entity\Node;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Psr\Log\LoggerInterface;
/**
* Provides a resource to get view modes by entity and bundle.
*
* #RestResource(
* id = "tio_rest_json_source",
* label = #Translation("Tio rest json source"),
* serialization_class = "Drupal\node\Entity\Node",
* uri_paths = {
* "canonical" = "/api/custom/",
* "https://www.drupal.org/link-relations/create" = "/api/custom"
* }
* )
*/
class TioRestJsonSource extends ResourceBase {
/**
* A current user instance.
*
* #var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* Constructs a new TioRestJsonSource object.
*
* #param array $configuration
* A configuration array containing information about the plugin
instance.
* #param string $plugin_id
* The plugin_id for the plugin instance.
* #param mixed $plugin_definition
* The plugin implementation definition.
* #param array $serializer_formats
* The available serialization formats.
* #param \Psr\Log\LoggerInterface $logger
* A logger instance.
* #param \Drupal\Core\Session\AccountProxyInterface $current_user
* A current user instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
array $serializer_formats,
LoggerInterface $logger,
AccountProxyInterface $current_user) {
parent::__construct($configuration, $plugin_id,
$plugin_definition, $serializer_formats, $logger);
$this->currentUser = $current_user;
}
/**
* {#inheritdoc}
*/
public static function create(ContainerInterface $container, array
$configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('import_json_test'),
$container->get('current_user')
);
}
/**
* Responds to POST requests.
*
* Returns a list of bundles for specified entity.
*
* #param $data
*
* #param $node_type
*
* #return \Drupal\rest\ResourceResponse
*
* #throws \Symfony\Component\HttpKernel\Exception\HttpException
* Throws exception expected.
*/
public function post($node_type, $data) {
// You must to implement the logic of your REST Resource here.
// Use current user after pass authentication to validate access.
if (!$this->currentUser->hasPermission('access content')) {
throw new AccessDeniedHttpException();
}
$node = Node::create(
array(
'type' => $node_type,
'title' => $data->title->value,
'body' => [
'summary' => '',
'value' => $data->body->value,
'format' => 'full_html',
],
)
);
$node->save();
return new ResourceResponse($node);
}
}
Now if i try to test this without passing a payload and modifing the return value in this way:
return new ResourceResponse(array('test'=>'OK'));
It's working!
But if i send a custom payload like this using my custom code above:
{
"title": [{
"value": "Test Article custom rest"
}],
"type": [{
"target_id": "article"
}],
"body": [{"value": "article test custom"}]
}
I recieve a 400 Error with: Symfony\Component\HttpKernel\Exception\BadRequestHttpException: The type link relation must be specified. in Drupal\rest\RequestHandler->handle() (line 103 of core/modules/rest/src/RequestHandler.php).
What's going Wrong?
Thx.
I have find a solution:
I have removed the annotation:
* serialization_class = "Drupal\node\Entity\Node",
Then i take care just for data in my post function:
/**
* Responds to POST requests.
*
* Returns a list of bundles for specified entity.
*
* #param $data
*
*
* #return \Drupal\rest\ResourceResponse
*
* #throws \Symfony\Component\HttpKernel\Exception\HttpException
* Throws exception expected.
*/
public function post($data) {
// You must to implement the logic of your REST Resource here.
// Use current user after pass authentication to validate access.
if (!$this->currentUser->hasPermission('access content')) {
throw new AccessDeniedHttpException();
}
return new ResourceResponse(var_dump($data));
The important thing is, when you use postman for example, is to add an header with Content-Type -> application/json:
Instead of Content-Type -> application/hal+json
With this configuration i can post any type of JSON and then manage it as i prefer.
Bye!

easy test for php function that take multiple objects as parameters

Hi Swagger/Restler friends,
How can I allow users to make easy test for php function and classes?
I have a class as follow:
class Author
{
/**
* #var string {#from body} {#min 3}{#max 100}
* name of the Author {#required true}
*/
public $name = 'Name';
/**
* #var string {#type email} {#from body}
* email id of the Author
*/
public $email = 'name#domain.com';
}
and I want to generate html documentation for a class that is as the follow:
class ComplexType {
/**
* post 2 Authors
*
* #param Author $author1
* #param Author $author2
*
* #return Author
*/
function post2Authors(Author $author1,Author $author2) {
return $author1;
}
}
It gives me when I run index.html the following to input:
{
"author1": "",
"author2": ""
}
But I need to view json input as follow:
{
"author1":
{
"name": "",
"email": ""
},
"author2": {
"name": "",
"email": ""
}
}
thank you in advance
Default value serves as an easy starter for trying the API with Restler API Explorer.
Currently it does not offer model parsing when more than one body parameter is found thus we are stuck with
{
"author1": "",
"author2": ""
}
We are working on support for Swagger 1.2 spec and along with that we will be releasing the full model parsing for default value along with that soon

Categories