API Platform - Which approach should I use for creating custom operation without entity - api-platform.com

I'm new to API Platform. I think it's great but I cannot find any example how to create custom endpoint that isn't based on any entity. There are a lot of examples based on an entity and usually they are all about CRUD. But what about custom operations?
I need to create custom search through database with some custom parameters which aren't related to any entity.
E.g. I want to receive POST request something like this:
{
"from": "Paris",
"to": "Berlin"
}
This data isn't saved to db and I haven't entity for it.
After I receive this data, there should be a lot of business logic including db queries through a lot of db tables and also getting data from external sources.
Then, after the business logic is finished, I want to return back result which is also custom and isn't related to any entity.
E.g.
{
"flights": [/* a lot of json data*/],
"airports": [/* a lot of json data*/],
"cities": [/* a lot of json data*/],
.......
}
So, I think I'm not the only on who does something similar. But I really cannot find a solution or best practices how to do this.
In the documentation I've found at least three approaches and I cannot implement none of them.
The best one, I guess the most suitable for me it is using Custom Operations and Controllers. But documentation says this one is not recommended. Also I think I should use DTOs for request and response, but for this approach I'm not sure I can use them.
The second one I found it's using Data Transfer Objects, but this approach requires an entity. According to the documentation, I should use DTOs and DataTransformers to convert DTO to an Entity. But I don't need entity, I don't need save it to db. I want just handle received DTO on my own.
The third one I guess it is using Data Providers, but I'm not sure it is suitable for my requirements.
So, the main question is which approach or best practice should I use to implement custom operation which isn't related to any entity. And it will be great use DTOs for request and response.

You are not forced to use entities. Classes that are marked with #ApiResource annotation may not be entities. Actually, if your application is smarter than basic CRUD you should avoid marking entities as ApiResource.
Since you want to use POST HTTP method (which is for creating resource items) you can do something like this.
1) Define class describing search fields and which will be your #ApiResource
<?php
// src/ApiResource/Search.php
namespace App\ApiResource;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Action\NotFoundAction;
use ApiPlatform\Core\Annotation\ApiProperty;
use App\Dto\SearchResult;
/**
* #ApiResource(
* itemOperations={
* "get"={
* "controller"=NotFoundAction::class,
* "read"=true,
* "output"=false,
* },
* },
* output=SearchResult::class
* )
*/
class Search
{
/**
* #var string
* #ApiProperty(identifier=true)
*/
public $from;
/** #var string */
public $to;
}
2) Define DTO that will represent the output
<?php
// src/Dto/SearchResult.php
namespace App\Dto;
class SearchResult
{
public $flights;
public $airports;
public $cities;
}
3) Create class that will inplement DataPersisterInterface for handling business logic.
It will be called by framework because you make POST request.
<?php
// src/DataPersister/SearchService.php
declare(strict_types=1);
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\Dto\SearchResult;
use App\ApiResource\Search;
final class SearchService implements DataPersisterInterface
{
public function supports($data): bool
{
return $data instanceof Search;
}
public function persist($data)
{
// here you have access to your request via $data
$output = new SearchResult();
$output->flights = ['a lot of json data'];
$output->airports = ['a lot of json data'];
$output->cities = ['inputData' => $data];
return $output;
}
public function remove($data)
{
// this method just need to be presented
}
}
That way you will recieve results based on request.

Related

Automatic filter on attribute depending on authenticated user

I have an entity called event, the event can have many rooms and a room can have many participants.
If I access all events (with a specific user) I can filter events where the user has no access right (no room with a connection to the specific user) by using extensions.
That works fine.
The response contains all events which have at least one room with access rights.
But If the event has multiple rooms and the user has only access to one room. The response includes both rooms. I created a RoomExtension, but this class will not be invoked.
Thanks
Your problem is caused by the fact that filters and extensions only work on the query that retrieves the primary entities. The related entities are retrieved using Doctrines associations wich are part of the domain model that is meant to be the single source of truth for all purposes. What you need is a user-specic view on that model, which in the context of api's usually consists of DTOs.
I think there are basically two solutions:
Query primarily for Events and convert the into EventDTOs, then either query for - or filter out - the related Rooms,
Query primarily for Rooms, then group them into EventDTOs.
I explain the second solution here because i guesss that it is simpeler and it shoud make your RoomExtension work out of the box, which makes it the better fit to your question, but also because i happen to have built and tested something similar in a tutorial so it is a lot less work to write an answer with confidence.
The downside of this solution is that it does not support pagination.
Bucause this solution primarily queries for Rooms, the the operation is on the Room resource. If it where the only collectionOperation of Room it could be like this:
(..)
* #ApiResource(
* collectionOperations={
* "get_accessible_events"={
* "method"="GET",
* "path"="/rooms/accessible-events",
* "output"=EventDTO::class,
* "pagination_enabled"=false
* }
* }
* }
*/
class Room {
(..)
(This does not have to be your only collectionOperation, you can still have "get", "post" and others).
Right now this still produces a flat collection of Rooms, you need to group them into EventDTOs. The DTOs page of the docs suggest to make a DataTransformer to produce the DTOs, but that only works if your DTOs are one to one with the entities retrieved by the query. But a CollectionDataProvider can do the trick. Because you do not need to adapt the query itself you can simply decorate the default CollectionDataProvider service:
namespace App\DataProvider;
use ApiPlatform\Core\Api\OperationType;
use App\DTO\EventDTO;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Entity\Room;
class RoomAccessibleEventCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface
{
/** #var CollectionDataProviderInterface */
private $dataProvider;
/**
* #param CollectionDataProviderInterface $dataProvider The built-in orm CollectionDataProvider of API Platform
*/
public function __construct(CollectionDataProviderInterface $dataProvider)
{
$this->dataProvider = $dataProvider;
}
/**
* {#inheritdoc}
*/
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Room::class === $resourceClass
&& $operationName == 'get_accessible_events';
}
/**
* {#inheritdoc}
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): array
{
$rooms = $this->dataProvider->getCollection($resourceClass, $operationName, $context);
$dtos = [];
foreach ($rooms as $room) {
$key = $room->getId();
if (isset($dtos[$key])) {
$dtos[$key]->addRoom($room);
} else {
$dto = new EventDTO($room->getEvent());
$dto->addRoom($room);
$dtos[$key] = $dto;
}
}
return $dtos;
}
}
You do need to configure the service in config/services.yaml:
'App\DataProvider\RoomAccessibleEventCollectionDataProvider':
arguments:
$dataProvider: '#api_platform.doctrine.orm.default.collection_data_provider'
This does not replace the default CollectionDataProvider but adds another one that gets the default one injected.
I guess you can make the EventDTO class yourself now. Then it should work. Filters defined on Room will also work as usual, for example if rooms can be filtered by the date of their event ?event.date[gte]=2020-10-10 will only find rooms with events on or after 2020-10-10 and return their EventDTO's.
However, in the swagger docs the get_accessible_events operations summary and descriptions still come from Room. You can look up how to add a SwaggerDecorator in the docs or take a look at the chapter9-api branch of the tutorial. The latter also contains complete explanations and tested code for entities, the DTO (Report Model) and an extension for only showing data the user is authorized for, but is not taylored to your questions and would all together be way beyond what a to the point answer.
I can not give you any more hints on this site with respect the other solution because this site will probably see them as an incomplete or unclear answer and punish me for it.

Why or when to use Laravel resources/collections

I do not understand why or when to use the resources in Laravel https://laravel.com/docs/7.x/eloquent-resources .
See this controller :
public function show(School $school)
{
// return response()->json($school, 200);
return new SchoolResource($school);
}
The both return solutions returned this kind of response :
{
"data": {
"id": "4f390a7b-3c3f-4c23-9e6a-dd4429cf835d",
"name": "school name",
.......,
The data are the results of a query automatically injected (here : $school).
And same question for a collection of resources. Imagine this controller :
public function index(Request $request)
{
try {
$schools = $this->schoolRepository->all($request->all());
} catch (\Exception $e) {
return response()->json(['message' => 'Bad request'], 400);
}
return SchoolResource::collection($schools);
// return response()->json($schools, 200);
}
If I need to add some fields , I can do that either in the model or in the repository.
I often read that this resource notion is important to understand and to use. But for the moment I do not see when or why I should use it. I certainly must not understand something!
There are a couple of primary reasons to use resources to manage your return values even if your resources don't do anything than pass data through today, you may want it to do something different in the future.
A small list of reasons why resources are really useful
1. Data manipulation, specific to clients (i.e. js applications consuming your api)
If you start manipulating data in your models using mutators (getters / setters). Your internal application now has to work with these constraints. Many times its easier to work with the raw data internally and then just allow your resources to manipulate the data for the client.
2. Conforming to an API specification, such as JSON API v1.0
Whilst you will likely need logic in your application to handle schemas like this, your models and controllers should not. The resource has a critical role here to organise the data for your consumer applications in a compliant fashion.
3. The age old mantra, separation of concerns
This goes hand in hand with point 1, it is not a model or a controllers responsibility to map data to what you expect your consumer applications to receive.
Building on your example
You currently have the following in the show route of your resource controller.
public function show(School $school)
{
// return response()->json($school, 200);
return new SchoolResource($school);
}
This need not change, even if your API specification does (reason 1 and 3).
Adding fields, yes, you'll need to add them to your model but lets use this to actually do something meaningful with your resource.
We've created a migration for a new JSON field ratings. Ratings has a data structure like this:
[
{
name: string,
value: float,
}
]
For reasons such as media scrutiny, we never want to expose all the rating data to our publically available front end consumer apps. Instead we want to provide an average score of all ratings.
Now we could do this in the model, but is it the models responsibility to do this? Not really, the model should be handling and dealing in raw / discreetly modified data only. So is it the controllers responsibility? No, the controller coordinates what should be done and is not interested in specific details or the data.
So where do we do this? Enter your resource that was handily already set up.
class School extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'rating' => $this->getRating(),
];
}
/**
* Mean rating rounded to one decimal place
* #return float
*/
protected function getRating()
{
return (round(array_reduce($this->ratings, function($acc, $curr) {
$acc += $curr['value'];
return $acc;
}, 0) / count($this->ratings), 1);
}
}
The resource is where we have manipulated data specifically for our responses and left our internal data modelling un-touched and clean without pollution from specific nuances of our consuming applications.
More importantly, if you just returned return response()->json($school, 200); from your controller, your data structures will not match and you would be exposing some sensitive data to your front end applications.
Additional (24/12/21)
It's worth noting that if, for example, the data that you are manipulating is required by many different views / resources then we have a number of options.
Traits, create a trait that adds the getRating method. Downside, every class that needs this must import and declare the trait. Upside, your code is dry.
Model scopes, add a scope to your model that does the data processing via SQL (or your DB QL of choice). Downside, slight pollution of the model. Upside, super quick.
Append the data to the model (https://laravel.com/docs/8.x/eloquent-serialization#appending-values-to-json) using an accessor that runs the getRating code to set the data on the model. Upside, clean and usable throughout the application. Downsides pollutes the model a little and data only available in the JSON representation of the model.
Decorate the resource. This allows you to intercept and modify/add to the result of the toArray method in the decorated resource. Upsides not many. Downsides, obfuscated and confusing implementation detail. I wouldn't recommend this approach.
Helper function. Rather than have the method on the resource, create a helper that takes the ratings array and returns the result. Upside, simple implementation. Downsides, none that I can thing of.
So after thinking about this alot I think that I would likely do what I originally wrote in this answer. If I need to re-use I would likely create a helper. If I was concerned about performance I would use a model scope to do the calculations in SQL (if possible, remember it's a JSON field). Taking a step further, if many models require this logic, a trait for those models would be my next step (this only applies if you go down the SQL calculation route).

Zend Framework 2 - Doctrine - Iterate all entity classes [duplicate]

This is probably pretty simple, but I can't find a way to do this.
Is there any way to get a list of class names of the entities that Doctrine manages? Something like:
$entities = $doctrine->em->getEntities();
where $entities is an array with something like array('User', 'Address', 'PhoneNumber') etc...
I know this question is old, but in case someone still needs to do it (tested in Doctrine 2.4.0):
$classes = array();
$metas = $entityManager->getMetadataFactory()->getAllMetadata();
foreach ($metas as $meta) {
$classes[] = $meta->getName();
}
var_dump($classes);
Source
Another way to get the class names of all entities (with namespace) is:
$entitiesClassNames = $entityManager->getConfiguration()->getMetadataDriverImpl()->getAllClassNames();
Unfortunately not, your classes should be organized in the file structure though. Example: a project i'm working on now has all its doctrine classes in an init/classes folder.
There is no built function. But you can use marker/tagger interface to tag entity classes that belong to your application. You can then use the functions "get_declared_classes" and "is_subclass_of" find the list of entity classes.
For ex:
/**
* Provides a marker interface to identify entity classes related to the application
*/
interface MyApplicationEntity {}
/**
* #Entity
*/
class User implements MyApplicationEntity {
// Your entity class definition goes here.
}
/**
* Finds the list of entity classes. Please note that only entity classes
* that are currently loaded will be detected by this method.
* For ex: require_once('User.php'); or use User; must have been called somewhere
* within the current execution.
* #return array of entity classes.
*/
function getApplicationEntities() {
$classes = array();
foreach(get_declared_classes() as $class) {
if (is_subclass_of($class, "MyApplicationEntity")) {
$classes[] = $class;
}
}
return $classes;
}
Please note that my code sample above does not use namespaces for the sake simplicity. You will have to adjust it accordingly in your application.
That said you did't explain why you need to find the list of entity classes. Perhaps, there is a better solution for what your are trying to solve.

Querying ORM with WHERE clause - Eloquent/Laravel 4

The relevant code is at: https://gist.github.com/morganhein/9254678
I have nested resource controllers, which query similarly structured tables in a database.
If I go to www.example.com/v1/jobs/1/departments, I want to query all departments that are associated with job 1. However I cannot figure out how to do that using the ORM.
Help?
I didn't test, but I would suggest you to try something like this:
Route::resource('/v1/jobs/{id}/departments', 'DepartmentController');
Route::resource('/v1/jobs', 'JobController');
After that, your DepartmentController methods will receive one argument, which is job id in your case. It is easier to use find() method if you are using id to retrieve any specific model. When you found the model you can get access to related models.
class DepartmentsController extends BaseController {
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index($jobId)
{
Clockwork::startEvent('List_Default_Departments', 'Starting.');
$positions = Auth::user()->jobs()->find($jobId)->departments;
Clockwork::endEvent('List_Default_Departments', 'Done.');
return Response::json($positions->toArray());
}
}
Note: There is a different between $job->departments() (returns Builder object to create more complex queries) and $job->departments (returns Collection object directly).
Also if you would like to get the list of jobs with all related departments you can always call:
$jobs = Auth::user()->jobs()->with('departments')->get();

Can I include a convenience query in a Doctrine 2 Entity Method?

I'd like to include some additional functions in my Doctrine 2 entities to contain code that I'm going to have to run quite frequently. For example:
User - has many Posts
Post - has a single user
I already have a function $user->getPosts(), but this returns all of my posts. I'm looking to write a $user->getActivePosts(), which would be like:
$user->getPosts()->where('active = true') //if this were possible
or:
$em->getRepository('Posts')->findBy(array('user'=>$user,'active'=>true)) //if this were more convenient
As far as I can tell, there's no way to get back to the entity manager though the Entity itself, so my only option would be
class User {
function getActivePosts() {
$all_posts = $this->getPosts();
$active_posts = new ArrayCollection();
foreach ($all_posts as $post) {
if ($post->getActive()) {
$active_posts->add($post);
}
}
return $active_posts;
}
However, this requires me to load ALL posts into my entity manager, when I really only want a small subset of them, and it requires me to do filtering in PHP, when it would be much more appropriate to do so in the SQL layer. Is there any way to accomplish what I'm looking to do inside the Entity, or do I have to create code outside of it?
I think you should implement the method on the PostRepository rather than on the entity model.
I try to keep all model related logic in the repositories behind "domain specific" methods. That way if you change the way you represent whether a post is active or not, you only have to change the implementation of a single method instead of having to find all the active = true statements scattered around in your application or making changes in an "unrelated" entity model.
Something like this
PostRepository extends EntityRepository {
public function findActiveByUser($user){
// whatever it takes to get the active posts
}
}

Resources