I was the simple api-platform resource configuration to access to list of user and just one user:
App\Entity\User:
collectionOperations:
get: ~
itemOperations:
get: ~
This config generate 2 routes:
/GET /api/users
/GET /api/users/{id}
One item of User that represent a user account and linked natural person:
{
"id": 0,
"username": "string",
"email": "string",
"person": {
"id": 0,
"civility": "string",
"lastName": "string",
"firstName": "string",
"language": "string",
"fullName": "string"
}
}
Then I want to update a specific field of my user account: password
/PUT /api/users/{id}/updatePassword
{
"password": "string",
"confirmPassword": "string"
}
My current api-platform configuration for this route is :
App\Resource\DTOs\UpdatePassword:
collectionOperations: []
itemOperations:
put:
method: 'PUT'
path: '/users/{id}/updatePassword'
requirements:
id: '\d+'
swagger_context:
tags: ['User']
summary: Update user account password
namespace App\Resource\DTOs;
class UpdatePassword
{
/** #var string */
public $password;
/** #var string */
public $confirmPassword;
}
The problem is that my DTOs don't know that it has to update a user. How to indicate that it is linked to a user resource? I tried with api-platform events but I don't know when to act.
I managed to do something that works but it's that I removed all the mechanisms of api-platform...
And more generally how to save several updates on the same resource ?
Do you have any idea?
I think the best way is to create a listener that will handled the persist part:
https://api-platform.com/docs/core/events/
class UpdatePasswordPersistSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => [['persist', EventPriorities::PRE_WRITE]],
];
}
public function persist(GetResponseForControllerResultEvent $event)
{
$object = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if ($object instanceof UpdatePassword && $method === Request::METHOD_PUT) {
// todo: persist
// Set the result in response if needed
// $event->setControllerResult($result);
}
}
}
I use something like that in a API Platform 2.2 version. Maybe you need to adapt for newer version.
Alternatively you can use a custom operation but that does not seem the best way:
https://api-platform.com/docs/core/operations/#creating-custom-operations-and-controllers
Related
I am trying to structure my project in Laravel just created to use it as a back-end API. I want all my responses from Laravel to be returned in the JSON:API format defined on the official site: https://jsonapi.org/format/
For example:
I have created the following 2 resource files:
1- php artisan make:resource User
2- php artisan make:resource UserCollection --collection
Two simple resource files, one to return a resource and one to return a collection.
Now, I would like to return in all my responses the following format:
In case of success
1- The status return code can be 200, 201 or 202.
2- The returned response should be similar to the following:
{
"data": [
{
"id": 1,
"email": "collins.elody#example.org"
}
],
"message": null,
"success": true
}
You may be wondering what is the point of passing the message key, in this case it is null because it would be returning a collection of records, that is, reading, but in case you needed to add a new record, update or delete one, you would need to pass a message to my front-end, in this case I would use that key for that.
Example, adding record, response status code 201:
{
"data": [],
"message": "Record created succesfully",
"success": true
}
In case of failure
As said here: https://jsonapi.org/format/#document-top-level : The members data and errors MUST NOT coexist in the same document.
So, in case of error, I need change data key by errors key, for example, suppose I am trying to authenticate myself, and the validation fails, in this case, it should turn out like this:
{
"errors": {
"email": "E-Mail is required",
"password": "Password is required"
},
"message": null,
"success": false
}
or I just want to return an error message, expected output should by:
{
"errors": [],
"message": "Something is Wrong!",
"success": false
}
So in essence what I need is a global wrapper for all the responses I make from Laravel. I would like to call return in an elegant way as follows:
return $this->success($collection);
or
return $this->success('Done!', 201);
So the first thing that came to mind was creating a trait and defining the methods you need to then call them from anywhere in Laravel
My Trait
<?php
namespace App\Traits;
trait APIResponse
{
public function success($data, $status = 200) {
return [
'data' => $data,
'success' => in_array($status, [200, 201, 202]) ? true : false,
'message' => null
];
}
public function failure($data, $status = 500) {
// TODO
}
}
My Controller
class ExampleController extends Controller
{
public function index() {
$collection = new UserCollection(User::all());
return $this->success($collection);
}
}
But I am not sure it is the right way to do it, please, someone skilled in the field who can help me. Thank you very much in advance.
You are on the right path, there is two main solutions i would consider best approaches, to handling your exact problem. Fractal and Eloquent Resources, i prefer Fractal due to some design decisions and experience.
I will show an example in fractal, using the wrapper by Spatie. Firstly create an serializer that will wrap the data as expected.
class YourCustomSerializer extends SerializerAbstract
{
public function collection($resourceKey, array $data)
{
return [
$resourceKey ?: 'data' => $data,
'message': null,
'success': true,
];
}
public function item($resourceKey, array $data)
{
return [
$resourceKey ?: 'data' => $data,
'message': null,
'success': true,
];
}
This should be added to your fractal.php config, there is published through the spatie wrapper.
Transforming your data you need a transformer.
class UserTransformer extends TransformerAbstract
{
public function transform(User $user)
{
return [
'name' => $user->name,
'email' => $user->email,
];
}
}
Now you can transform your data into the expected format.
public function response($data, int $statusCode = Response::HTTP_OK)
{
return fractal($data, $this->transformer)->respond($statusCode);
}
For error codes, you should go to the Handler.php and add something similar to this. This is very naive way of doing it, but for know should get you going on error handling, you need to do something with validation exception, status code etc.
if ($request->wantsJson()) {
return response()->json([
'success' => false,
'message' => $exception->getMessage(),
], Response::HTTP_BAD_REQUEST);
}
I am trying to convert data from json to DTO class.
I am using Symfony serializer.
But when I am trying to deserialize. It does not parse type from typed property, I guess Symfony does not support deserialization form typed property yet. Is it so? Do I have to implement my one?
DTO:
class ElkUser
{
public string $partnerUuid;
public string $contractUuid;
public DealerInfo $dealerInfo;
}
class DealerInfo
{
public string $description;
public int $dealerId;
public string $dealerName;
public bool $enabled;
public string $registrationDate;
}
Serializer config:
$normalizers = [
new DateTimeNormalizer(),
new ObjectNormalizer(
null,
null,
null,
new ReflectionExtractor
),
];
$serializer new Serializer($normalizers, [new JsonEncoder()]);
The test case when give me error:
TypeError : Typed property App\Services\CreditPilot\ElkUser::$dealerInfo must be an instance of App\Services\CreditPilot\DealerInfo, array used
$json = <<< JSON
{
"partnerUuid": "string",
"contractUuid": "string",
"dealerInfo": {
"dealerId": 0,
"dealerName": "string",
"enabled": true,
"registrationDate": "2020-03-10T12:49:08.367Z",
"contract": {
"contractNumber": "string",
"enabled": true,
"creationDate": "2020-03-10T12:49:08.367Z"
}
}
}
JSON;
$serializer->deserialize($json, ElkUser::class, 'json');
This will be supported natively in Symfony 5.1:
The PropertyInfo component extracts information about the properties of PHP classes using several sources (Doctrine metadata, PHP reflection, PHPdoc config, etc.) In Symfony 5.1, we improved this component to also extract information from PHP typed properties.
Before that, you need to give some information to the serializer so it's able to infer the type. A PhpDoc or a typed setter could be enough.
Is there any solutions on how to change property names or attributes of JSON Request in Laravel?
Something like Eloquent API Resources but instead in responses, it will be done in requests before directing to the request validation?
From this,
{
"agent_reference": "ABC-12345",
"product_instance_id": "aca68c65-44c3-4ea1-a726-ca183de09a31",
"add_ons": [
"string"
],
"transportation": "string",
"guests": [
{
"guest_type_key": "string",
"add_ons": [
"string"
],
"field_responses": [
{
"key": "string",
"response": "string"
}
]
}
]
}
To this,
{
"agent_id": "ABC-12345",
"plan_id": "aca68c65-44c3-4ea1-a726-ca183de09a31",
"additional_params": [
"string"
],
"pickup_place": "string",
"visitors": [
{
"visitor_kty": "string",
"additional_params": [
"string"
],
"responses": [
{
"id": "string",
"result": "string"
}
]
}
]
}
Laravel 4 had support for such a thing, however, sadly, it got removed with the release of Laravel 5.
For Laravel 5.2+, the "neat" way of achieving this is is by overriding the validationData() method in your App\Http\Requests\FormRequest class.
Take a look at:
Illuminate\Foundation\Http\FormRequest
protected function validationData()
{
return $this->all();
}
The function above is meant to return all inputs that will be sent to the validation, $this is the Request itself.
So, in your App\Http\Requests\FormRequest class, you define the very same method, get the inputs, sanitize as you want (even creating a whole new structure with different key names, as requested in your question), replace it in the request to persist and return, like so:
/**
* Get data to be validated from the request.
*
* #return array
*/
protected function validationData()
{
$inputs = $this->all();
// Sanitize $inputs as your likes.
$this->replace($inputs); // To persist.
return $this->all();
}
This is how you get it done in Laravel 5.2+.
Hope it helps.
My question concerns various process of json payload validations.
I have recensed :
- deserialization on a model, calling validator service and validate the hydrated object.
- using FormType (even if there are no forms...just json feeds) and validate the form builder after injecting $datas.
Which one do you prefer ?
Have you a better solution ? Such as maybe a middleware (unique bundle ou app that deals with all in/out-coming payloads - request/response)
Thank You
I validate/deserialize with the native listeners/tools FOSRestBundle provides.
Making use of the bundle you can have native form-validation ... or automatically deserialized and validated models plus a list of validation errors injected as controller arguments.
# app/config/config.yml
# You need SensioFrameworkExtraBundle for body converters to work
sensio_framework_extra:
request: { converters: true }
fos_rest:
zone:
- path: '^/api/(.*)+$'
# [..]
body_listener:
enabled: true
default_format: json
decoders:
json: fos_rest.decoder.jsontoform
# automatically injects query parameters into controller Actions
# see #FOSRest\QueryParam in the example below
param_fetcher_listener: force
# https://symfony.com/doc/master/bundles/FOSRestBundle/request_body_converter_listener.html
body_converter:
enabled: true
validate: true
validation_errors_argument: validationErrors
The body converter can deserialize and validate models automatically for you (without using any forms or manual steps). Example:
/**
* #ParamConverter(
* "post",
* converter = "fos_rest.request_body",
* options = {
* "validator" = {
* "groups" = {
* "validation-group-one",
* "validation-group-two",
* }
* },
* "deserializationContext" = {
* "groups" = {
* "serializer-group-one",
* "serializer-group-two"
* },
* "version"="1.0"
* }
* }
* )
*/
public function putPostAction(Post $post, ConstraintViolationListInterface $validationErrors)
{
if (!empty($validationErrors)) {
// return some 4xx reponse
}
// Do something with your deserialized and valid Post model
The bundle can serialize forms (and form-errors) to JSON, too.
i.e. a form with invalid fields will be rendered as:
{
"code": 400,
"message": "Validation Failed",
"errors": {
"errors": [
"This is a global form error."
],
"children": {
"oldPassword": {
"errors": [
"The old password is not correct."
]
},
"newPassword": [],
"submit": []
}
}
}
FOSRestBundle provides a request body listener that automatically decodes Content-Type: application/json to Content: application/x-www-form-urlencoded within the Request object so you can bind the request to the form with handleRequest as you'd do with normal HTML forms.
Quick tip: if you just want to validate your data asynchronously ... you can send the request with a query param (?validate=true in the following example) and return an early response with HTTP 200 (OK) / 202 (Accepted) before performing any business logic.
The following example shows an endpoint that accepts requests of the form:
{
"oldPassword": "xxxxxxx",
"newPassword": "yyyyyyy"
}
Corresponding controller action:
/**
* #FOSRest\Route(
* "/profile/change-password",
* name="api_put_password",
* methods={
* Request::METHOD_PUT
* }
* )
*
* #FOSRest\QueryParam(
* name="validate",
* allowBlank=false,
* default="false",
* strict=true,
* nullable=true,
* requirements="^(true|false)$"
* )
*/
public function putPasswordAction(Request $request, string $validate = 'false')
{
$validate = filter_var($validate, FILTER_VALIDATE_BOOLEAN);
$form = $this->formFactory->createNamed(null, ChangePasswordType::class, null, [
'action' => $this->router->generateUrl('api_put_password'),
'method' => $request->getMethod(),
]);
$form->handleRequest($request);
if (!$form->isValid()) {
$view = new View();
$view->setStatusCode(Response::HTTP_BAD_REQUEST);
$view->setData($form);
return $view;
}
if ($validate) {
$view = new View();
$responseCode = Response::HTTP_ACCEPTED;
$view->setStatusCode($responseCode);
$view->setData([
'code' => $responseCode,
'message' => 'Data is valid.',
'data' => null
]);
return $view;
}
$user = $this->securityContext->getToken()->getUser();
/** #var PasswordChangeRequest $passwordChangeRequest */
$passwordChangeRequest = $form->getData();
$user->setPassword($this->passwordEncoder->encodePassword($user, $passwordChangeRequest->getNewPassword()));
$this->userManager->persist($user);
$view = new View();
$view->setStatusCode(Response::HTTP_OK);
$view->setData([
'code' => Response::HTTP_OK,
'message' => 'Password changed successfully.',
'data' => $user
]);
$context = new Context();
$context->setGroups([
'profile'
]);
$view->setContext($context);
return $view;
}
What I have coded is a oneToMany relationship with doctrine
one user ---> has many notifications
This is how I get the data
/**
* #Route("/test")
*/
public function testRoute()
{
//get the user notifications
$notifications = $this->getUser()->getNotifications();
//return json response
$serializer = $this->get('serializer');
$json = $serializer->serialize($notifications, 'json');
$response = new Response($json);
$response->headers->set('Content-Type', 'application/json');
return $response;
}
This is what the controller returns
[
{
"id": 1,
"receiver": 1,
"notification_type": "new_comment",
"triggered_by": {
"id": 1,
"username": "gabriel",
"username_canonical": "gabriel",
"password": "3e6bS2I==",
"email": "ga#ga.de",
"first_name": "Gabriel",
"last_name": "ThaKid",
"likes_counter": 0,
"dislikes_counter": 2,
"favourites_counter": 0,
"profile_pic": "profilepic_60181.png",
"salt": "Lqch0N84UH1QmFI5O",
"form_token": "sO6NgWd",
"is_active": true,
"registration_confirmation": "success",
"secret_confirmation_id": "qTwNGm4CSKHzJOe8ry9DcXavt",
"socket_token": "KuMlxYHa"
},
"created_at": "2014-12-16T13:36:20+0100",
"link_to": "#test"
},
{
"id": 2,
"receiver": 1,
"notification_type": "new_comment",
"triggered_by": {
"id": 1,
"username": "gabriel",
"username_canonical": "gabriel",
"password": "3e6bS2IYX1DONLA/70a8hzMUQ==",
"email": "ga#ga.de",
"first_name": "Gabriel",
"last_name": "ThaKid",
"likes_counter": 0,
"dislikes_counter": 2,
"favourites_counter": 0,
"profile_pic": "profilepic_60181.png",
"profile_rank": "Beginner", (...)
},
"created_at": "2014-12-16T13:36:24+0100",
"link_to": "#test"
}
]
I think you get the point, it returns the notifications of a certain user which is allright,
I also need certain fields from the user, like the lastname and the firstname, so the returned data is usable in the application.
But doctrine also returns the hashed password and the salt along with tokens and information the user doesn't need to know.
how do I tell doctrine not to return those fields or not to fetch them to begin with?
This is not about Doctrine, this is the default Serializer that tries to fetch and return all of the values available.
Take a look at the Serializer component documentation to understand how to ignore properties. Basically, you'll have to pass a normalizer into your serializer's constructor:
<?php
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
$normalizer = new GetSetMethodNormalizer();
$normalizer->setIgnoredAttributes(array('age'));
$encoder = new JsonEncoder();
$serializer = new Serializer(array($normalizer), array($encoder));
$serializer->serialize($person, 'json');
Also, I'd strongly suggest to switch to JMSSerializer and/or use FOSRestBundle, as it gives a lot more flexibility for the serialization. The property list is configured against contexts, and has base exclusion strategies. This way, you'd only need to list properties to expose, not to exclude:
AppBundle\Entity\Car:
exclusion_policy: all
properties:
id:
expose: true
vendor:
expose: true
title:
expose: true
In the given example, all properties that were not listed would be excluded.
You should create a custom repository class.
In this class create a method like this:
// src/AppBundle/Entity/NotificationRepository.php
namespace AppBundle\Entity;
use Doctrine\ORM\EntityRepository;
class NotificationRepository extends EntityRepository
{
public function findAllByUser()
{
return $this->getEntityManager()
->createQuery(
'SELECT [only the fields you need] FROM [....]'
)
->getResult();
}
}
For the DQL query you could use the partial object syntax.