Skip to content

RFC: A "Expression" Action for RAD #5307

@Richard87

Description

@Richard87

Description
In this example we wouldnt need a basic class to get the item object, the input dto, and call the function, we could define it directly :)

Cons:

  • No autocomplet/typehints in the expression
  • a litte more complex API-Platform
  • A new distinction, item for the API Resource, and input for the DTO

Pros:

  • Slightly improved DX
  • Rapid application development improvements

Example
An Action could look like this:

new Action(
    input: CreateAreaDto::class,
    expression: 'item.createArea(input.county)',
    uriTemplate: '/partnerships/{id}/create_area',
    security: "is_granted('ROLE_ADMIN')",
),

A proof of concept look like this:

<?php

namespace App\Service\ApiPlatform;

use ApiPlatform\Metadata\HttpOperation;
use App\Controller\ActionController;

#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
class Action extends HttpOperation
{
    private string $expression;

    public function __construct(
        string $name,
        string $input,
        string|false|null $output,
        string $expression,
        string $uriTemplate,
        string|array|null $uriVariables,
        string $security,
        string|null $securityPostDenormalize,
    ) {
        parent::__construct(
            method: 'POST',
            uriTemplate: $uriTemplate,
            uriVariables: $uriVariables,
            controller: ActionController::class,
            security: $security,
            securityPostDenormalize: $securityPostDenormalize,
            input: $input,
            output: $output,
            name: $name
        );

        $this->expression = $expression;
    }

    public function getExpression(): string
    {
        return $this->expression;
    }
}

And the controller:

<?php

namespace App\Controller;

use ApiPlatform\State\CallableProvider;
use App\Service\ApiPlatform\Action;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\HttpFoundation\Request;

class ActionController
{
    private CallableProvider $provider;

    public function __construct(
        #[AutoWire(service: 'api_platform.state_provider')] CallableProvider $provider,
        private EntityManagerInterface $em,
    ) {
        $this->provider = $provider;
    }

    public function __invoke(Request $request, $data)
    {
        $attributes = $request->attributes;
        /** @var Action $operation */
        $operation = $attributes->get('_api_operation');
        $className = $attributes->get('_api_resource_class');
        $id = $attributes->get('id');


        if (!$id || !$className || !$operation) {
            throw new \RuntimeException('Invalid action, missing operation, className or id');
        }

        $repo = $this->em->getRepository($className);
        $object = $repo->find($id);
        $expressionLanguage = new ExpressionLanguage();

        return $expressionLanguage->evaluate($operation->getExpression(), ['item' => $object, 'input' => $data]);
    }
}

The current approach have a few limitations:

  • I struggled to create optional arguments for the action (some strange error :O ) so currently all specified are required
  • Requires id the the uriTemplate, no support for other types of identifiers (hardcoded)

I really like the separation of item and input, the current solution with object, data and previous_data is confusing when you need to deal with it...

I would love to solve these issues with tighter integration in API-Platform Core :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions