Skip to content

Better Ability to handle different models under one resource #2224

Closed
@ragboyjr

Description

@ragboyjr

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).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions