How to use a DTO and Messenger with Api-Platform? - php

For one of our resources, we'd like to route the request through a command handler. Creating a new item for this resource has a number of side-effects and prior requirements, so regular REST is not enough.
Since we already have a Messenger handler written for use in different parts of the application, and a simple command object, we thought to use Messenger with an Input Object, as described in the documentation, that says:
Set the messenger attribute to input, and API Platform will automatically dispatch the given Input as a message instead of the Resource. Indeed, it'll add a default DataTransformer (see input/output documentation) that handles the given input
My resource is described thus:
App\Domain\Entity\Bid:
input: 'App\Application\Command\Lead\BidOnLead'
messenger: "input"
itemOperations:
get: ~
collectionOperations:
get: ~
post: ~
But, when I load the api-docs the schema I'm shown as input is the schema for App\Domain\Entity\Bid, not the schema for App\Application\Command\Lead\BidOnLead, as I would expect.
In case it was only a matter of the documentation being generated incorrectly, I've tried sending a JSON describing the input object (it's a very simple object with a couple of properties), but it fails because it's trying to deserialize into the resource object, not into the input object.
What I'm doing wrong, and how can I implement this?

I haven't tested this, but per documentation here:
https://api-platform.com/docs/core/messenger/#symfony-messenger-integration-cqrs-and-async-message-processing
input should be under attributes key:
App\Domain\Entity\Bid:
attributes:
input: 'App\Application\Command\Lead\BidOnLead'
messenger: "input"
itemOperations:
get: ~
collectionOperations:
get: ~
post: ~
It may be misleading if you just copying things from annotations to yaml as documentation for yaml is not thorough in ApiPlatform.

Related

Is it possible to load Swagger annotations from a different class or file?

I have the following simple PHP method like the following
/**
*
* (swagger annotation to be called from a different class)
*
*/
public function getApiCall()
{
//Do something
}
and I need to include long Swagger documentation into the annotation above the method, so
is it possible to write the annotation in a different class ? and call it here with something like
/**
*
*call('App\Http\Controllers\testAnnotation');
*/
The main purpose is to have a clean class without so many lines of documentation and annotations in it.
Loading "annotations from a different class" is not something that makes a lot of sense. Annotations are read in the annotated code, that's their whole purpose.
But if you want to keep configuration and code separated, you do not have to use Swagger-Php to generate your swagger configuration file.
The package is simply a convenience way to generate the swagger.json file from code annotations.
But if you do not want to use annotations in the first place, and keep your classes clean from extraneous configuration (something that I personally applaud), just... do not use Swagger-Php and build your own configuration files outside of your classes.
You could even write it in YAML, if you feel more comfortable than writing JSON by hand. For example::
openapi: 3.0.0
info:
title: 'Search API'
version: 1.0.0
servers:
- url:
description: Current host server
- url: https:your-server.com
description: Prod server
paths:
/foo:
post:
summary: 'Creates a new foo'
description: 'Builds a new Foo and makes it available to Bar'
requestBody:
description: 'Foo '
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Foo'
responses:
'201':
description: Foo created
'202':
description: Foo queued, it will be eventually created.
components:
schemas:
Foo:
type: object
required:
- name
- size
properties:
name:
type: string
size:
type: integer
This, once converted to JSON (there are many libraries to do this, or you could even use a free service like this one), you can feed the resulting JSON to swagger directly.
E.g. the YAML above parses to this JSON file. You can easily test it out by heading to the Swagger demo instance, and past the JSON URL in the "explore" location bar, and you'll get something like this:
In the end, it's not much more work than using annotations (if any more work at all), and you can keep your entity classes clean from configuration concerns.

How does the Google Service Client (PHP) method documentation work?

I have consistent difficulty using any client service method that is not explicitly exampled somewhere. Despite following the docs and even reading the sourcecode, The class or method names I come up with following the scheme are never right.
The documentation at Packagist (see 'Making Requests") says the client library classes are autogenerated from the Google endpoints, which agrees with the description in the library's docs on Github that say the pattern for accessing methods should be "$service->resource->method(args)".
So why the following?
// works:
// I get a countable object of active classrooms owner by the specified id
$response = $this->ClassroomService->courses->listCourses([
'courseStates' => 'ACTIVE',
'teacherId' => 'me']);
// works:
// I get an instance of the single classroom's object containing lots of meta data
$response = $this->ClassroomService->courses->get( $id );
// does not work:
// 500 error, obj has no such method
$response = $this->ClassroomService->topics->listCoursesTopics( $id );
According to the API Explorer all three should be fine.
What am I missing about using client service objects?
Edit
Ultimately I determined the resource in my example to be 'courses_topics'; the method was correct per the docs. Thanks for the idea #ebram.
The question remains how the methods are named though. courseWork is my next challenge and it does not fit the the naming pattern of topics.
There is no topics member of ClassroomService.
The member is named courses_topics.
This is documented in the "Properties summary" at the bottom of the Classroom documentation.
Your code should look like this:
$response = $this->ClassroomService->courses_topics->listCoursesTopics( $id );
The documentation for Google_Service_Classroom_CoursesTopics_Resource does incorrectly give sample code where the member is named ->topics.
Given that API documentation is generated from source, but sample code is (generally) written by hand, I'll assume the API documentation is correct and the sample code is incorrect. I'd file a documentation-bug with Google.
Update:
I looked at the PHP source code for Google_Service_Classroom in GitHub and verified that the property is named courses_topics instead of topics, so in conclusion: the sample code is wrong.
What's also interesting is the resource-type in the actual source-code is Google_Service_Classroom_Resource_CoursesTopics but the documentation refers to it as Google_Service_Classroom_CoursesTopics_Resource - so that documentation is definitely wrong.

Testing SLIM appication against API Specification with PHPUnit

I have an API, written in PHP with Slim (3, will be upgraded to 4 soon), I also have an extensive specification written in openapi3 YML format (I could convert to another format if necessary, but I think it would be the best to keep oas3). Currently I am testing all endpoint against this specification with Dredd. This is a Tool which goes through a API specification an, sends example data to the real API and checks if the result matches the spec. That works, but not very good. For a test run I have to wipe out and re-initialize the db with PHP and then run Dredd with npm. Plus since Dredd does not support all features of oas3 I have to do some conversion first.
Since I have some Unit-tests with PHPUnit anyway, I would love to run the other tests with PHPUnit as well (and get rid of the Node-stuff at all). I found http://opensource.byjg.com/php-swagger-test/ and https://github.com/Maks3w/SwaggerAssertions. Both of them provide this very feature, but I would have to write a separate function for every endpoint, containing sample data and so on - stuff which is already in the API spec. Any Idea how I could avoid this afford and just use my API spec as test defintion with PHPUnit or at least any PHP library?
Example from the specs:
/users:
get:
summary: get a list of users
description: get info about all registered users - in whole application or in a workspace.
parameters:
- in: header
name: AuthToken
schema:
$ref: '#/components/schemas/auth'
example:
at: 132token
- in: query
name: ws
description: id of a workspace to get users from. can be omitted for all users in system
required: false
examples:
a:
value: 0
b:
value: 1
With byjg php-swagger-test I would have to write something like this
public function usersA()
{
$request = new \ByJG\Swagger\SwaggerRequester();
$request
->withMethod('GET')
->withPath("/users")
->withHEader(blabla)
->withRequestBody(['ws'=>0]);
$this->assertRequest($request);
}
public function usersB()
{
$request = new \ByJG\Swagger\SwaggerRequester();
$request
->withMethod('GET')
->withPath("/users")
->withHEader(blabla)
->withRequestBody(['ws'=>1]);
$this->assertRequest($request);
}
Two tests (functions) containing only information which is already in the spec. Is there a better tool/way to run tests over all endpoints against the spec without writing all those?

Why/how does this work: Symfony 2.8 + FOSRestBundle automatically deserializes request object

I am working on migrating an existing Symfony 2.8 which I did not create. The previous developers are not available for questions.
Most of the code easy to understand, but at one point I simply cannot figure out how are why this works:
A controller/action parameter is automatically de-serialized from JSON data to a custom object. This is nothing special, I do not understand how/where the Symfony is told what to do. Form my point of view important config data is missing but it works anyway.
That's would be fine, but without understanding why it works in this case, I cannot figure out why it does not work anymore when migrating the project to Symfony 3.4...
Sorry for the (very) long question, but I tried different ways to solve the problem and answer the questions but they all resulted in different problems...
Using the following Bundles
sensio/framework-extra-bundle v3.0.29
friendsofsymfony/rest-bundle 2.4.0
jms/serializer 1.13.0
jms/serializer-bundle 1.5.0
Config:
// app/config/config.yml
sensio_framework_extra:
request: { converters: true }
fos_rest:
...
body_converter:
enabled: true
#jms_serializer: (not configured)
# ...
Code:
// src/AppBundle/Util/SortInfo.php
class SortInfo {
public $sortOrder;
public $sortBy;
}
// src/AppBundle/Resources/serializer/Util.SortInfo.yml
AppBundle\Util\SortInfo:
exclusion_policy: ALL
properties:
sortOrder:
type: string
expose: true
sortBy:
type: string
expose: true
// src/AppBundle/Controller/SortingController.php
class SortingController extends Controller {
...
/**
* #FOSRest\View()
*/
public function sortAction(SortInfo $sortInfo) {
$this->logger->info("sortAction");
$this->logger->info(" ".gettype($sortInfo)." -- ".get_class($sortInfo));
$this->logger->info(" ".$sortInfo->sortOrder." -- ".$sortInfo->sortBy);
...
}
}
That's it. After digging for some hours I could not find any explicit configuration that would tell the sortAction method to convert the JSON data from the request automatically. But it works.
A request with the following JSON content results in the following log output:
// JSON content
{"sortOrder":"ASC", "sortBy":"name"}
// Log output
sortAction
object -- AppBundle\Util\SortInfo
ASC -- name
Why does this work?
As said before this is fine, but the problem is, that the same code does not work with Symfony 3.4 and newer versions of FOSRest, ExtrasBundle and JMSSerializer:
sensio/framework-extra-bundle v5.2.4
friendsofsymfony/rest-bundle 2.5.0
jms/serializer 2.1.0
jms/serializer-bundle 3.0.0
A request with the same JSON data results in the following log output in `Symfony 3.4':
// Log output
sortAction
object -- AppBundle\Util\SortInfo
--
So although the SortInfo is created (not null), the sortBy and sortOrder properties are empty/null.
How can this be? I do not understand why the data is de-serialized in the first place, but how it is possible to create a SortInfo object without setting the properties is even more mysterious.
In Symfony 2.8 the Util.SortInfo.yml file is necessary for the process. Removing this files results in an exception:
Symfony\Component\HttpKernel\Exception\BadRequestHttpException: "You
must define a type for
AppBundle\Util\SortInfo::$sortBy."
at /.../vendor/friendsofsymfony/rest-bundle/Request/RequestBodyParamConverter.php
Here I do not understand why SortInfo::$sortBy is a problem but not SortInfo it self. Additionally removing the file in Symfony 3.4 does not change anything.
But at least this error message confirms, that the FOS RequestBodyParamConverter is involved in the process.
Following the Symfony docs it should be necessary to manually specify that the parameter that should be converted (although it works just fine without this config in Symfony 2.8):
/**
* #FOSRest\View()
* #ParamConverter("sortInfo", converter="fos_rest.request_body")
*/
public function sortAction(SortInfo $sortInfo) {
...
}
However, adding this config results in another error in Symfony 3.4:
Uncaught PHP Exception RuntimeException: "Converter
'fos_rest.request_body' does not support conversion of parameter
'$sortInfo'
Bottom line:
This is all very mysterious.
Why does it work without any problem in Symfony 2.8 although regarding to the docs essential config is missing (explicitly specify ParamConverter)
Why does it NOT work in Symfony 3.4?

Symfony2 + FOS RestBundle: Filter/clear JSON request before applying ParamConverter

I am working on a Symfony 2 WebApp that provides a JSON webservice to communicate with mobile apps on different platforms (iOS, Android, etc.).
The WebApp uses the FOS RestBundle to process and deserialize the received the JSON data:
# app/config/config.yml
sensio_framework_extra:
request: { converters: true }
fos_rest:
...
body_converter:
enabled: true
/**
* Sync Controller implementation
* #ParamConverter("post", converter="fos_rest.request_body")
*/
public function syncAction(SyncBag $syncBag) {
// ...
}
/**
* Sync Data Wrapper
*/
public class SyncBag {
...
/**
* #JMS\Type("DateTime<'Y-m-d'>")
*/
private $startDate;
}
This works quite well. The Problem: Some of the mobile Apps use a wrong date format to send some of the data: While the WebApp expects "Y-m-d" the mobile Apps send "Y-m-d H:i:s". This leads to an exception:
Uncaught PHP Exception
Symfony\Component\HttpKernel\Exception\BadRequestHttpException:
"Invalid datetime "2016-03-04 12:00:00", expected format Y-m-d." at
/.../vendor/friendsofsymfony/rest-bundle/FOS/RestBundle/Request/AbstractRequestBodyParamConverter.php
line 113
Of course updating the mobile apps to use/send the correct date format is the correct solution for this error. But implementing and publishing this bugfix will take quite a while. Passing Apples review will take some time. Waiting for the users to update the latest version will take some time. And so on.
On the other hand the problem could quite easily be solved by updating the deserializing process in the WebApp.
Question:
Is it somehow possible to hook into the deserializing process within the FOS RestBundle ParamConverter? If I could filter/clean the request (simpley search/replace wrong date formats) before the data is handled by the Converter, the problem could be solved within minutes.
Any idea/suggestion how to do this?
Thanks!
Why not create your own ParamConverter extending/using what you need from the existing ones (e.g. in your case from the built in DateTime ParamConverter, see documentation)
You can even set a priority for ParamConverters to be sure one is executed before another.
Or is there anything special you'd like to achieve here?
You're can create event listener for pre deserialize event
Example for JMSSerializer
This is dispatched before an object is deserialized. You can use this
to modify submitted data, or modify the type that is being used for
deserialization.
Also, you're can create listener for kernel.request (or for kernel.controller with priority bigger than param converter listener priotity) event, and filter data as need.

Categories