ConverterHandler.php

PHP

Path: src/Controller/ConverterHandler.php

<?php

namespace mini\Controller;

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
 * Request handler that converts controller method return values to PSR-7 responses
 *
 * This handler wraps a controller method callable and automatically converts its
 * return value to a ResponseInterface using the converter registry.
 *
 * Conversion flow:
 * 1. Invoke the controller method with URL parameters from request attributes
 * 2. If return value is already ResponseInterface, return it directly
 * 3. Otherwise, use converter registry to convert return value to ResponseInterface
 * 4. If no converter found, throw RuntimeException
 *
 * This enables controllers to return any type (arrays, strings, domain objects)
 * without manually creating Response objects.
 *
 * Example:
 * ```php
 * // Controller method returns array
 * public function index(): array {
 *     return ['users' => $this->users];
 * }
 *
 * // ConverterHandler converts array → ResponseInterface via registered converter
 * $handler = new ConverterHandler($this->index(...));
 * $response = $handler->handle($request); // JSON response
 * ```
 *
 * @package mini\Controller
 */
class ConverterHandler implements RequestHandlerInterface
{
    /**
     * @param callable $handler Controller method or callable to invoke
     */
    public function __construct(
        private readonly mixed $handler
    ) {}

    /**
     * Handle the request by invoking the controller method and converting its return value
     *
     * @param ServerRequestInterface $request
     * @return ResponseInterface
     * @throws \RuntimeException If return value cannot be converted to ResponseInterface
     */
    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        // Invoke controller method with parameters from request attributes
        $result = $this->invokeHandler($request);

        // Already a response? Return directly
        if ($result instanceof ResponseInterface) {
            return $result;
        }

        // Try to convert using converter registry
        $response = \mini\convert($result, ResponseInterface::class);

        if ($response === null) {
            throw new \RuntimeException(
                "Controller method returned " . get_debug_type($result) .
                " which cannot be converted to ResponseInterface. " .
                "Either return ResponseInterface directly or register a converter for this type."
            );
        }

        return $response;
    }

    /**
     * Invoke the handler with dependency injection from request attributes
     *
     * @param ServerRequestInterface $request
     * @return mixed The controller method return value
     */
    private function invokeHandler(ServerRequestInterface $request): mixed
    {
        $handler = $this->handler;

        // Get reflection for parameter analysis
        if ($handler instanceof \Closure) {
            $reflection = new \ReflectionFunction($handler);
        } elseif (is_array($handler)) {
            $reflection = new \ReflectionMethod($handler[0], $handler[1]);
        } else {
            $reflection = new \ReflectionMethod($handler);
        }

        $args = [];

        // Build arguments from request attributes (URL parameters set by Router)
        foreach ($reflection->getParameters() as $param) {
            $name = $param->getName();

            // Get from request attributes (Router stores URL parameters here)
            $value = $request->getAttribute($name);

            if ($value !== null) {
                $args[] = $value;
                continue;
            }

            // Use default value if available
            if ($param->isDefaultValueAvailable()) {
                $args[] = $param->getDefaultValue();
                continue;
            }

            // Allow null if parameter is nullable
            if ($param->allowsNull()) {
                $args[] = null;
                continue;
            }

            throw new \InvalidArgumentException("Missing required parameter: $name");
        }

        return call_user_func_array($handler, $args);
    }
}