core icon indicating copy to clipboard operation
core copied to clipboard

Customize controller for Laravel

Open Dasc3er opened this issue 3 months ago • 1 comments

Description

Following the documentation, with Symphony it is possible to override the controller or rely on provider/processor systems, while the Laravel integration is limited to provider/processors only. This can also be verified with a simple test case in Laravel.

This seems hard coded here: https://github.com/api-platform/core/blob/325a04c8303c25f04fd0cc107664965bc46b0c16/src/Laravel/routes/api.php#L49

Would it be possible to allow custom controllers here, for use cases where a bit more flexibility is generally helpful? There should also be a documented way to correctly serialize/deserialize contents for this case.

Dasc3er avatar Nov 20 '25 21:11 Dasc3er

I am able to get the feature mostly working with the some slight changes:

  • Update api.php to allow custom controllers
  • Update ApiPlatformController.php to allow custom actions for the body generation (to ensure better code reuse)
  • Create new ApiPlatformCustomController.php as an abstract class extending ApiPlatformController.php, to be extended by all custom controllers

I am currently finding an issue that the serializer by the default processor is for some reason not detecting the format correctly, leading to the following error: Serialization for the format \"\" is not supported - any help to solve this would be useful.

Details on my changes below.

On api.php:

  • Add following line 48 to identify if a custom controller is set on the Operation attribute
$with_custom_controller =
                    $operation->getController() &&
                    $operation->getController() !=
                        "api_platform.action.not_exposed";
  • Change the following code https://github.com/api-platform/core/blob/325a04c8303c25f04fd0cc107664965bc46b0c16/src/Laravel/routes/api.php#L49 to
"uses" => $with_custom_controller
                                ? $operation->getController()
                                : ApiPlatformController::class,

To reuse as much code as possible from ApiPlatformController.php https://github.com/api-platform/core/blob/325a04c8303c25f04fd0cc107664965bc46b0c16/src/Laravel/Controller/ApiPlatformController.php#L80

Edit to

        $body = $this->customProvider($request, $operation, $uriVariables, $context);

and add method

    protected function customProvider(Request $request, HttpOperation $operation, array $uriVariables = [], array $context = []) {
        return $this->provider->provide($operation, $uriVariables, $context);
    }

Then create ApiPlatformCustomController.php

// Controller/ApiPlatformController.php

declare(strict_types=1);

namespace ApiPlatform\Laravel\Controller;

use ApiPlatform\Metadata\HttpOperation;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

abstract class ApiPlatformCustomController extends ApiPlatformController
{
    abstract public function handler(
        Request $request,
        HttpOperation $operation,
    ): mixed;

    public function __invoke(Request $request): Response
    {
        $operation = $request->attributes->get("_api_operation");
        if (!$operation) {
            throw new \RuntimeException("Operation not found.");
        }

        if (!$operation instanceof HttpOperation) {
            throw new \LogicException("Operation is not an HttpOperation.");
        }

        $operation->withRead(false);
        $operation->withWrite(true);

        return parent::__invoke($request);
    }

    protected function customProvider(
        Request $request,
        HttpOperation $operation,
        array $uriVariables = [],
        array $context = [],
    ) {
        return $this->handler($request, $operation);
    }
}

This will allow the following logic to work:

use ApiPlatform\Metadata\Get;
use Illuminate\Http\Request;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Laravel\Controller\ApiPlatformCustomController;

class CustomControllerTestResponse
{
    public bool $test;
}

#[
    Get(
        uriTemplate: "/test/{id}",
        controller: CustomControllerTest::class,
        output: CustomControllerTestResponse::class,
    ),
]
final class CustomControllerTest extends ApiPlatformCustomController
{
    public function handler(
        Request $request,
        HttpOperation $operation,
    ): CustomControllerTestResponse {
        $response = new CustomControllerTestResponse();
        $response->test = true;
        return $response;
    }
}

As noted, this will lead to error Serialization for the format \"\" is not supported, which is caused by the ApiPlatformMiddleware not finding or filling the _format attribute and/or the default processor not falling to a default for this case. More details to follow whenever I have time to check in depth, or if anyone is able to take a look.

This can be verified by manually changing the fallback _format from empty to jsonld which will get everything working correctly. https://github.com/api-platform/core/blob/325a04c8303c25f04fd0cc107664965bc46b0c16/src/Laravel/ApiPlatformMiddleware.php#L45

Dasc3er avatar Nov 22 '25 16:11 Dasc3er