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;
}
Related
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
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
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!
I am currently working with the NelmioApiDocBundle, with which I am not very familiar yet. The API I am writing has to provide a route to change the password of a specific user. The documentation should state, that for changing the password both the old one and the new one are required. Since I did not found an explanation of the difference between Requirementsand Parameters, I guess the first is used for data from the route and the latter is used for the API call itself.
First attempt of archieving such a documentation was to implement a simple Model, which the JMSSerializerBundle then automatically converts:
class ChangePasswordParam
{
/**
* #Type("string")
* #var string
*/
protected $oldPassword;
/**
* #Type("string")
* #var string
*/
protected $newPassword;
}
The Controller accepts the API call via this action method:
/**
* Changes the password for a specific user.
*
* #Post("/{username}/changepassword")
* #View()
* #ApiDoc(
* description="Changes the password of a User",
* input="FQCN\ChangePasswordParam"
* )
*
* #param string $username
* #param ChangePasswordParam $passwordParam
*
* #return Response
*/
public function changePasswordAction($username, ChangePasswordParam $passwordParam)
{
/* ... */
}
This led to the documentation showing username as Requirement, old_password and new_password as Parameter. To mark those Parameters as required, I added a Symfony Constraint via annotation to the properties:
class ChangePasswordParam
{
/**
* #Type("string")
* #Assert\NotNull()
* #var string
*/
protected $oldPassword;
/**
* #Type("string")
* #Assert\NotNull()
* #var string
*/
protected $newPassword;
}
However, while using these annotations marked the properties as required, it does generate strange output:
Notice the parameters being added twice and in different formats? Adding the #SerializedName("old_password") has no effect. Regarding this ticket, the issue is still not solved.
Another way of accepting data for the action is using a custom form, which indeed marks the properties as required but also generates no proper output. Changing the ChangePasswordParam as custom form:
class ChangePasswordParam extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('old_password', 'text');
$builder->add('new_password', 'text');
}
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName()
{
return 'change_password';
}
}
is resulting in this parameter description:
Those parameters should be named just 'old_password' and 'new_password' and I can't figure out how to archieve this.
Thanks in advance
Your #ApiDoc annotation should include an empty input form name field like below:
* #ApiDoc(
* description="Changes the password of a User",
* input= {
* "class" = "FQCN\ChangePasswordParam",
* "name" = ""
* }
* )
This will remove the form name before the parameters name.
I want to map the URL with tonic, how can I do that? is there any particular doc where I can? except for its API doc, because I have read it's API there are no any docs about that?
for example here is my class
/**
* Display and process a HTML form via a HTTP POST request
*
* This page outputs a simple HTML form and gathers the POSTed data
*
*
*
*
* #uri /example/getExample/:name
* #uri /example/getExample/
*
* #uri /Example/:name
*
*/
class example extend Resource{
/**
*
* Handle a GET request for this resource
* #method GET
* #param Request $name
* #return str
*/
function getExamplemethod1($name=null) {
return json_encode(array('status'=>'done method 2'));
}
/**
*
* Handle a POST request for this resource
* #method GET
* #param Request $name
* #return str
*/
function getExamplemethod2($name=null) {
if($name == null){
return json_encode(array('status'=>'done1'));
}
else{
return json_encode(array('status'=>'done1', 'name'=>$name));
}
}
}
I want to call the method in the URL how can I do that?