Symfony Serializer: Deserializing Json to Entity - php

I am trying to use Symfony's Serializer to deserialize a Json to my entity "DossierDTO".
class DossierDTO
{
#[Groups(['test'])]
public string $idActeurCreateur;
#[Groups(['test'])]
public string $idDossierVise;
#[Groups(['test'])]
public string $idProjet;
public ArrayCollection $personnes;
public ArrayCollection $terrains;
.
.
.
more fields
I would like to deserialize only the fields tagged with the #[Groups(['test'])] annotations.
Here is my call to fetch the json object and my attempt to deserialize it:
/**
* Make a request to API
* #param string $method: request method (POST, GET...)
* #param string $suffix: URI suffix (/example)
* #param array $body: request body
* #throws Exception
* #return ResponseInterface
*/
public function myRequest(string $method, string $suffix, ?array $body): ResponseInterface
{
$jsonContent = is_null($body) ? json_encode(new stdClass) : $this->serializer->serialize($body, 'json');
try {
$response = $this->client->request($method, $this->infos["uri"] . $suffix, [
'headers' => $this->infos["headers"],
'body' => $jsonContent
]);
} catch (Exception $e) {
$this->logger->error($e->getMessage());
}
$dossier = $this->serializer->deserialize($response->getContent(), DossierDTO::class, 'json', ["groups" => "test"]);
dd($dossier, $response->getContent());
}
And this is what my dump shows:
So basically, I don't get the fields that I would like to, even when I remove the "#[Groups(['test'])]" the result is the same.
It always shows me the two ArrayCollection fields (empty) and only these...
I'm working with Symfony 5.2.9

From your screenshot I can see that response JSON has nested object, keyed by "projet". Looks like you are mapping incorrect structure. Try this:
$this->serializer->deserialize($response->getContent()['projet'], DossierDTO::class, 'json', ["groups" => "test"]);

Related

Wrapping JSON response in an abstracted way in PHP / Laravel

I am making a REST API that will return different JSON responses depending on which type of User is making the call.
There is a single endpoint: example.com/api/v1/collect that uses Laravel's API authentication to get the User model with $user = auth()->guard('api')->user();.
Each User will belong to a Type.
If User 1 (type_id 1) makes the call, the response will look like:
{
"example": 1,
"success": true,
"data" : [
...
]
}
If User 2 (type_id 2) makes the call, the response can be different, depending on the user's type. It could look like:
{
"example": 2,
"response" : [
...
],
"custom": "string",
"success": 200
}
The ... is the data that we are sending back (for example a list of Post titles) and it will always be the same, but the "envelope" (or wrapper) around it would be specific to each user (or type of user).
So far, I've found two solutions to wrap that ... in an abstracted way:
Solution 1: Using Laravel Blade
// Api\V1\ApiController.php
$data = $user->posts->pluck('title');
// Each type of user will have a different blade filename
// There could be around a 100 types which will result in a 100 blade files
// The filename is stored in the database
$filename = $user->type->filename; // returns 'customBladeTemplate'
// Return a JSON response after passing the $data to the view
return response()->json([
view($filename, compact('data'))->render(),
]);
Using a blade file for each type of user allows me to wrap the data like this:
// resources/views/customBladeTemplate.blade.php
// This filename has to match the one in the database column
{
"example": 1,
"success": true,
"data" : [
{!! $data !!}
]
}
That will output a JSON response for the User 1 (example 1)
Solution 2: Using Laravel response macros
// Api\V1\ApiController.php
$data = $user->posts->pluck('title');
// Each type of user will have a different macro name
// There could be around a 100 types which will result in a 100 different macros
// The macro name is stored in the database
$macroName = $user->type->macro_name; // returns 'customMacroName'
return response()->{macroName}($data);
Creating a Macro for each type of user, using the macro name from the DB:
// App\Providers\AppServiceProvider.php
use Illuminate\Http\Response;
public function boot()
{
Response::macro('customMacroName', function ($data) {
return Response::json([
'example' => 2,
'response' => $data,
'custom' => 'string',
'success' => 200,
]);
});
}
That macro will output a JSON response for the User 2 (example 2)
Both options work fine but I am still wondering:
Is there another (possibly better) way to do it?
Are those two solutions valid or can they be enhanced?
Which of those two solutions seem to be better and why?
Edit: The $data is not actually coming from an eloquent model, it is rather from a serialized JSON column (JSON casting) - which means I can't use the Laravel API resources
If you are looking for the response formatting you should go with the Laravel API Resources
Based on your requirement(data formate different for two type of users), you can create two different Api Resource classes.
AdminResource & UserResource.
Here you have more flixibility on controlling fields or orgnizing data.
Here is how you can define the resource class:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
class UserResource extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
And you can use this as:
use App\User;
use App\Http\Resources\UserResource;
Route::get('/user', function () {
return new UserResource(User::find(1));
});
If you would like to include with condition check in with user type, you can create a common function called renderJson($userType, $data) and place this in your parent class or can wrap with traits, all depends on your application architecture.
Here you can find laravel documentation for API Resource: https://laravel.com/docs/5.8/eloquent-resources
Edited:
With Laravel API Resource, you not only parse the modal object, you can parse any arrayble object.
Essentially they are just simple objects with one very important job
to do — transform your objects (interesting I said objects and not
models). To do this out of the box, all you have to do is instantiate
the Resource (collection or individual) with an Arrayable object. If
you did nothing else but generate a standard Resource and pass in an
Arrayable object the Resource would transform that object
automatically, and because Models are Arrayable this is where I got
caught out because if you create a resource collection and instantiate
it with a collection of models then the models get toArray'd and not
their corresponding resource.
Src: https://medium.com/#dinotedesco/laravel-api-resources-what-if-you-want-to-manipulate-your-models-before-transformation-8982846ad22c
So in your case if you can just collect() the json data and pass to api resource.
You could use middlewares to change what the response looks like.
With middleware you could change the response after the execution of your regular code, without having to take this into account in the controller itself. Using the below code you modify the response AFTER it has been executed.
<?php
namespace App\Http\Middleware;
use Closure;
class AfterMiddleware
{
public function handle($request, Closure $next)
{
// Calls the controller and processes the request.
$response = $next($request);
// Here you can retrieve the user and modify the response however you want.
// Some example code:
$user = Auth::user();
if ($user->type == 1) {
... //Change response for user type 1
}
if ($user->type == 2) {
... //Change response for user type 2
}
// Etc...
return $response;
}
}
Reference: https://laravel.com/docs/5.8/middleware
Depends on how different the responses are from each other. I'm inclined to take inventory of each type's common features and build a response array as appropriate. This could be done in the controller or a helper function and then returned using Laravel's JSON response type.
$response = [];
// results common to all types
$response['example'] = $example;
$response['success'] = $success;
// customized results for specific types
if (in_array($type, [1, 3, 4, 5, ...])) {
$response['data'] = $dotdotdot;
}
if (in_array($type, [2, 6, 7, 8, ...])) {
$response['response'] = $dotdotdot;
$response['custom'] = $custom;
}
return response()->json($response);
I don't know if this is what you are looking for. I had something similiar a few months ago and fixed it with json files. As json is amazingly fast and you can create thousands of types.
Sorry for my bad english i will fix it after the weekend :-)
Let's get started.
First the user logs in using laravel passport or api routes.
Second the api calls a controller.(class). I will create a class based on your info.
let's say the api calls the ApiController and the method handle
use Illuminate\Http\Request;
class ApiController
{
public function __construct()
{
}
/**
* Handle the incoming request
*
* #param Request $request
*/
public function handle(Request $request)
{
//first let's find the correct format
$type = $requets->user()->type; //lets say type_1
$config = $this->getUserType($type);
//i don't know where you data comes from but let's say $data is your data.
$data = json_encode(['some' => "data", 'to' => "return"]);
//lets fill the data
$response = $this->fillDataInConfig($data, $config);
return response()->json($response);
}
/**
* Find the user type config by type name
*
* #param string $type
* #return object
*/
private function getUserType(string $type) : string
{
//let store them in the local storage
$config = \Storage::disk('local')->get("api_types/$type.json");
return json_decode($config);
}
/**
* Fill the data
*
* #param mixed $data
* #param object $config
* #return object
*/
private function fillDataInConfig($data, object $config) : object
{
//as for your example. The reusl//
// {
// "example": 2,
// "response" : *responseData*, <===== where the response should be
// "custom": "string",
// "success": 200
// }
foreach($config as $index => $value){
if($value === '*responseData*'){
$config->{$idnex} = $data;
}
}
//the data is filled in the response index
return $config;
}
}

FOSRestBundle EntityType not working in JSON

I am using Symfony 3.4 and FOSRestBundle for my APIs.
All the services are working fine except this one where I am posting an entity with a form and an EntityType field.
Controller:
public function createAssistanceCallAction(Request $request)
{
$assistanceCall = new AssistanceCall();
$form = $this->createForm(AssistanceCallType::class, $assistanceCall);
$form->handleRequest($request);
dump($form->isSubmitted(), $form->isValid());die;
}
Entity property:
/**
* #var MobileAppUser
*
* #ORM\ManyToOne(targetEntity="MobileAppUser")
* #ORM\JoinColumn(name="mobile_app_user_id", referencedColumnName="id", nullable=false)
* #Assert\NotBlank
*/
protected $mobileAppUser;
Form:
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('mobile_app_user', EntityType::class, array(
'class' => 'AppBundle:MobileAppUser',
))
->add('save', SubmitType::class)
;
}
It's working fine with a normal POST:
curl -X POST http://mysite.local/api/users/1/create-assistance-call -F 'assistance_call[mobile_app_user]=26'
dump($form->isSubmitted(), $form->isValid());die; // true and true
It's not working with JSON format:
curl -X POST \
http://mysite.local/api/users/1/create-assistance-call \
-d '{
"assistance_call": {
"mobile_app_user": {
"id": 1
}
}
}'
dump($form->isSubmitted(), $form->isValid());die; // false and false
What am I doing wrong in the JSON example?
Just to add to the first answer, you also need to modify your JSON payload.
You have to put directly your mobile_app_user id :
{
"assistance_call": {
"mobile_app_user": 1
}
}
Since you are sending your json object as a request body and not as normal POST fields - you should json_decode request content first and then use $form->submit(...) to load your form with the data. The old good handleRequest() won't do here. Take a look at the following example:
public function createAssistanceCallAction(Request $request)
{
$assistanceCall = new AssistanceCall();
$form = $this->createForm(AssistanceCallType::class, $assistanceCall);
//Notice the "true" argument passed to the json_decode
$data = json_decode($request->getContent(), true);
$form->submit($data);
dump($form->isSubmitted(), $form->isValid());die;
}
You can also use FOSRestBundle BodyListener to decode json for you. To do that - add the following configuration entry:
fos_rest:
body_listener:
decoders:
json: fos_rest.decoder.jsontoform
This still doesn't make $form->handleRequest() a good choice since it will only allow one method per form, so if you configure your form to do POST - the PUT requests will always fail without any explicit error messages.
So then you would amend the code above like this:
public function createAssistanceCallAction(Request $request)
{
$assistanceCall = new AssistanceCall();
$form = $this->createForm(AssistanceCallType::class, $assistanceCall);
$form->submit($request->request->all());
dump($form->isSubmitted(), $form->isValid());die;
}
Pay attention to the Content-Type header you send as if it is set to 'multipart/form-data' while having json payload in its body - decoder will fail.

Testing Stripe in Laravel

I'm creating a subscription-based SaaS platform in Laravel, where Laravel Cashier does not suit my needs. Therefore I need to implement the subscription-engine myself using the Stripe library.
I found it easy to implement the connection between Laravel and Stripe via hooking into the creation and deletion events of a Subscription class, and then create or cancel a Stripe subscription accordingly.
The Stripe library is unfortunately largely based on calling static methods on some predefined classes (.. like \Stripe\Charge::create()).
This makes it hard for me to test, as you normally would allow dependency injection of some custom client for mocking, but since the Stripe library is referenced statically, there is no client to inject. Is there any way of creating a Stripe client class or such, that I can mock?
Hello from the future!
I was just digging into this. All those classes extend from Stripe's ApiResource class, keep digging and you'll discover that when the library is about to make an HTTP request it calls $this->httpClient(). The httpClient method returns a static reference to a variable called $_httpClient. Conveniently, there is also a static method on the Stripe ApiRequestor class called setHttpClient which accepts an object which is supposed to implement the Stripe HttpClient\ClientInterface (this interface only describes a single method called request).
Soooooo, in your test you can make a call to ApiRequestor::setHttpClient passing it an instance of your own http client mock. Then whenever Stripe makes an HTTP request it will use your mock instead of its default CurlClient. Your responsibility is then have your mock return well-formed Stripe-esque responses and your application will be none the wiser.
Here is a very dumb fake that I've started using in my tests:
<?php
namespace Tests\Doubles;
use Stripe\HttpClient\ClientInterface;
class StripeHttpClientFake implements ClientInterface
{
private $response;
private $responseCode;
private $headers;
public function __construct($response, $code = 200, $headers = [])
{
$this->setResponse($response);
$this->setResponseCode($code);
$this->setHeaders($headers);
}
/**
* #param string $method The HTTP method being used
* #param string $absUrl The URL being requested, including domain and protocol
* #param array $headers Headers to be used in the request (full strings, not KV pairs)
* #param array $params KV pairs for parameters. Can be nested for arrays and hashes
* #param boolean $hasFile Whether or not $params references a file (via an # prefix or
* CURLFile)
*
* #return array An array whose first element is raw request body, second
* element is HTTP status code and third array of HTTP headers.
* #throws \Stripe\Exception\UnexpectedValueException
* #throws \Stripe\Exception\ApiConnectionException
*/
public function request($method, $absUrl, $headers, $params, $hasFile)
{
return [$this->response, $this->responseCode, $this->headers];
}
public function setResponseCode($code)
{
$this->responseCode = $code;
return $this;
}
public function setHeaders($headers)
{
$this->headers = $headers;
return $this;
}
public function setResponse($response)
{
$this->response = file_get_contents(base_path("tests/fixtures/stripe/{$response}.json"));
return $this;
}
}
Hope this helps :)
Based off Colin's answer, here is an example that uses a mocked interface to test creating a subscription in Laravel 8.x.
/**
* #test
*/
public function it_subscribes_to_an_initial_plan()
{
$client = \Mockery::mock(ClientInterface::class);
$paymentMethodId = Str::random();
/**
* Creates initial customer...
*/
$customerId = 'somecustomerstripeid';
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params, $opts) use ($paymentMethodId) {
return $path === "https://api.stripe.com/v1/customers";
})->andReturn([
"{\"id\": \"{$customerId}\" }", 200, []
]);
/**
* Retrieves customer
*/
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params) use ($customerId) {
return $path === "https://api.stripe.com/v1/customers/{$customerId}";
})->andReturn([
"{\"id\": \"{$customerId}\", \"invoice_settings\": {\"default_payment_method\": \"{$paymentMethodId}\"}}", 200, [],
]);
/**
* Set payment method
*/
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params) use ($paymentMethodId) {
return $path === "https://api.stripe.com/v1/payment_methods/{$paymentMethodId}";
})->andReturn([
"{\"id\": \"$paymentMethodId\"}", 200, [],
]);
$subscriptionId = Str::random();
$itemId = Str::random();
$productId = Str::random();
$planName = Plan::PROFESSIONAL;
$plan = Plan::withName($planName);
/**
* Subscription request
*/
$client->shouldReceive('request')
->withArgs(function ($method, $path, $params, $opts) use ($paymentMethodId, $plan) {
$isSubscriptions = $path === "https://api.stripe.com/v1/subscriptions";
$isBasicPrice = $opts["items"][0]["price"] === $plan->stripe_price_id;
return $isSubscriptions && $isBasicPrice;
})->andReturn([
"{
\"object\": \"subscription\",
\"id\": \"{$subscriptionId}\",
\"status\": \"active\",
\"items\": {
\"object\": \"list\",
\"data\": [
{
\"id\": \"{$itemId}\",
\"price\": {
\"object\": \"price\",
\"id\": \"{$plan->stripe_price_id}\",
\"product\": \"{$productId}\"
},
\"quantity\": 1
}
]
}
}", 200, [],
]);
ApiRequestor::setHttpClient($client);
$this->authenticate($this->user);
$res = $this->putJson('/subscribe', [
'plan' => $planName,
'payment_method_id' => $paymentMethodId,
]);
$res->assertSuccessful();
// Actually interesting assertions go here
}

Symfony Api Rest which validator process

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;
}

Symfony: How to render a template as a simple txt

I have to render a template of an action as a simple .txt file.
How can I do this? Is there a way other than using the Response object?
Using a Response object:
$content = $this->get('templating')->render(
'AppBundle:Company:accountBillingInvoice.txt.twig',
[
'invoice' => 'This is the invoice'
]
);
$response = new Response($content , 200);
$response->headers->set('Content-Type', 'text/plain');
I can't see what's wrong with using the Response object - it's pretty simple!
If you want to render text responses from many controller actions and you don't want to repeat yourself a lot, you can define some service class that builds up the response for you, like:
class TextResponseRenderer
{
/** #var EngineInterface */
private $engine;
// constructor...
/**
* #param string $template The name of the twig template to be rendered.
* #param array $parameters The view parameters for the template.
* #return Response The text response object with the content and headers set.
*/
public function renderResponse(string $template, array $parameters): Response
{
$content = $this->engine->render($template, $parameters);
$textResponse = new Response($content , 200);
$textResponse->headers->set('Content-Type', 'text/plain');
return $textResponse;
}
}
Other option may be writing a listener for the kernel.response that modifies the response headers, but this might be over-complicating things. See more info here.

Categories