Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better Ability to handle different models under one resource #2224

Closed
ragboyjr opened this issue Oct 1, 2018 · 2 comments
Closed

Better Ability to handle different models under one resource #2224

ragboyjr opened this issue Oct 1, 2018 · 2 comments

Comments

@ragboyjr
Copy link
Contributor

ragboyjr commented Oct 1, 2018

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

@ragboyjr
Copy link
Contributor Author

ragboyjr commented Oct 6, 2018

I've been able to implement this in a separate bundle here: https://github.com/krakphp/api-platform-extra#operation-resource-classes

Would like to submit a PR to the core because the changes are pretty minor and I think offer a great deal of ease when configuring operations with different resource classes.

@dunglas
Copy link
Member

dunglas commented Dec 22, 2018

A very similar approach is now supported: #2235

@dunglas dunglas closed this as completed Dec 22, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants