Description
There are times (as mentioned several times throughout API Platform) when you want to use a separate models for different endpoints for a specific resource.
I've found for most of my entities that aren't simple CRUD, I typically need to overwrite 1-3 endpoints so that I can setup custom services for handling those endpoints which handle the business logic in creation. Typically it's the POST, DELETE, and PUT endpoints that I'd end up having a special service manage for a specific entity (most of the time, I just need to override POST).
My current way of managing this works, but it has a bunch boilerplate which is needed to allow API platform to properly generate the swagger documentation.
App\Entity\CapturedPayment:
collectionOperations:
get: ~
App\DTO\CapturePaymentRequest:
itemOperations: []
collectionOperations:
post:
path: /captured-payments
swagger_context:
tags: ["CapturedPayment"]
summary: "Capture a payment"
responses:
201:
description: "Payment was captured"
schema: { $ref: "#/definitions/CapturedPayment" }
Then, I have a custom data persister that looks like the following:
<?php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\DTO;
use App\Exception\RequestHandlerNotFound;
use App\Service;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
class AppDataPersister implements DataPersisterInterface, ServiceSubscriberInterface
{
private $container;
private $persister;
public function __construct(ContainerInterface $container, DataPersisterInterface $persister) {
$this->container = $container;
$this->persister = $persister;
}
/**
* Is the data supported by the persister?
*
* @param mixed $data
*
* @return bool
*/
public function supports($data): bool {
return (is_object($data) && $this->container->has(get_class($data))) || $this->persister->supports($data);
}
/**
* Persists the data.
*
* @param mixed $data
*
* @return object|void Void will not be supported in API Platform 3, an object should always be returned
*/
public function persist($req) {
$requestClass = get_class($req);
if (!$this->container->has($requestClass) && !$this->persister->supports($req)) {
throw new RequestHandlerNotFound($requestClass);
} else if (!$this->container->has($requestClass)) {
return $this->persister->persist($req);
}
$handleReq = $this->container->get($requestClass);
return $handleReq($req);
}
/**
* Removes the data.
*
* @param mixed $data
*/
public function remove($data) {
return $this->persister->remove($data);
}
public static function getSubscribedServices() {
return [
DTO\CapturePaymentRequest::class => Service\CapturePayment::class,
];
}
}
Now, this system works pretty good, but it's quite complicated and hard to explain to new devs on what's going on because it's a lot of boilerplate to implement something straight forward.
For the record, i'm pretty sure I could ditch the AppDataPersister, and just use a custom controller action which then calls the service, but that also seemed like overkill in regards to creating a custom controller class along with the service class.
Proposed Solution
I think an easy solution to this would be to extend operation schema in config to allow a custom resource model:
App\Entity\CapturedPayment:
collectionOperations:
get: ~
post:
resource_class: App\DTO\CapturedPayment
Then, we could add (likely in a separate scope of work), I could add a MessageBusDataPersister
that routes those entities into the message bus and we could just piggy back off of the MessageBus's awesome implementation for tagging handlers.
Proof of Concept
I was able to get a basic PoC working by doing the following in my resource yaml config:
App\Entity\CapturedPayment:
collectionOperations:
get: ~
post:
defaults:
_api_resource_class: SG\Svc\SalesChannel\Core\DTO\Payment\CapturePaymentRequest
swagger_context:
parameters:
- in: body
name: capturedPaymentRequest
required: true
schema: { $ref: "#/definitions/CapturePaymentRequest" }
# This is needed so that the swagger docs gen to add this into swagger models
App\DTO\CapturePaymentRequest:
itemOperations: []
collectionOperations:
post: ~
This works pretty well, but you need to update https://github.com/api-platform/core/blob/master/src/Bridge/Symfony/Routing/ApiLoader.php#L212 to allow the $options['defaults']
to override the defaults provided by API Platform (using array_merge would fix that issue).