Handlers & Invocation Strategies

The Slim\Handlers namespace provides the default ErrorHandler responsible for HTTP content negotiation, exception logging, and response generation, alongside the four route callback invocation strategies under Slim\Handlers\Strategies. Together, these classes govern how matched Route callables are invoked and how uncaught Throwable instances are converted into PSR-7 responses by ErrorMiddleware.

05_handlers_and_strategies_1
Slim\Handlers\ErrorHandler Execution & Content Negotiation Pipeline
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.styles import Colors, Styles
from drawlib.text import text

setup(width=180, height=76)

# Outer container for ErrorHandler
rectangle(
    (90, 36),
    width=168,
    height=56,
    style=Styles.Neutral.patch(shape_r=3.0),
)
text(
    (90, 68.5),
    "Slim\\Handlers\\ErrorHandler::__invoke(request, exception, ...)",
    style=Styles.PrimaryBold.patch(text_size=12.5),
)

# Step 1: Input
rectangle(
    (24, 42),
    width=30,
    height=22,
    style=Styles.PrimaryFlat.patch(shape_r=2.0),
)
phosphor.warning_octagon((24, 47.5), width=6.0, style=Styles.White)
text((24, 40.5), "ErrorMiddleware", style=Styles.WhiteBold.patch(text_size=10.5))
text((24, 35.5), "Request + Throwable", style=Styles.White.patch(text_size=10.0))

# Step 2: Status & Content-Type Negotiation
rectangle(
    (68, 49),
    width=38,
    height=15,
    style=Styles.PrimaryNeutral.patch(shape_r=2.0),
)
text((68, 52.0), "determineStatusCode()", style=Styles.PrimaryBold.patch(text_size=10.5))
text((68, 46.5), "OPTIONS:200 | Http:Code | 500", style=Styles.Dark.patch(text_size=10.0))

rectangle(
    (68, 25),
    width=38,
    height=15,
    style=Styles.PrimaryNeutral.patch(shape_r=2.0),
)
text((68, 28.0), "determineContentType()", style=Styles.PrimaryBold.patch(text_size=10.5))
text((68, 22.5), "Accept Header / Forced MIME", style=Styles.Dark.patch(text_size=10.0))

# Step 3: Logger & Renderer Resolution
rectangle(
    (116, 49),
    width=36,
    height=15,
    style=Styles.SecondaryNeutral.patch(shape_r=2.0),
)
text((116, 52.0), "writeToErrorLog()", style=Styles.SecondaryBold.patch(text_size=10.5))
text((116, 46.5), "PlainText -> PSR-3 Logger", style=Styles.Dark.patch(text_size=10.0))

rectangle(
    (116, 25),
    width=36,
    height=15,
    style=Styles.PrimaryNeutral.patch(shape_r=2.0),
)
text((116, 28.0), "determineRenderer()", style=Styles.PrimaryBold.patch(text_size=10.5))
text((116, 22.5), "Resolve MIME Renderer", style=Styles.Dark.patch(text_size=10.0))

# Step 4: Output Response
rectangle(
    (157, 37),
    width=28,
    height=24,
    style=Styles.AccentFlat.patch(shape_r=2.0),
)
phosphor.file_code((157, 43.0), width=6.0, style=Styles.White)
text((157, 35.5), "respond()", style=Styles.WhiteBold.patch(text_size=11.0))
text((157, 30.0), "PSR-7 Response", style=Styles.White.patch(text_size=10.0))

# Connectors
line((39, 46), (49, 49), arrow_head="->", style=Styles.PrimaryBold)
line((39, 38), (49, 25), arrow_head="->", style=Styles.PrimaryBold)
line((87, 49), (98, 49), arrow_head="->", style=Styles.SecondaryBold)
line((87, 25), (98, 25), arrow_head="->", style=Styles.PrimaryBold)
line((134, 49), (143, 41), arrow_head="->", style=Styles.SecondaryBold)
line((134, 25), (143, 33), arrow_head="->", style=Styles.PrimaryBold)

save()

Summary of Classes#

Class Namespace Implements Primary Role
ErrorHandler Slim\Handlers ErrorHandlerInterface Negotiates Accept content type, logs exceptions via PSR-3, and renders HTTP error responses.
RequestResponse Slim\Handlers\Strategies InvocationStrategyInterface Default strategy: attaches route params as request attributes and passes ($request, $response, array $args).
RequestResponseArgs Slim\Handlers\Strategies InvocationStrategyInterface Unpacks route parameters positionally via ($request, $response, ...array_values($args)).
RequestResponseNamedArgs Slim\Handlers\Strategies InvocationStrategyInterface Unpacks route parameters as PHP 8+ named arguments via ($request, $response, ...$args).
RequestHandler Slim\Handlers\Strategies RequestHandlerInvocationStrategyInterface Invokes PSR-15 request handler callables as $callable($request) with optional attribute injection.

classSlim\Handlers\ErrorHandler#

ErrorHandler is Slim's default application error handler invoked by ErrorMiddleware. It determines the appropriate HTTP status code, negotiates the response media type against the request's Accept header (supporting JSON, XML, HTML, and Plain Text via Slim\Error\Renderers), optionally writes formatted diagnostics to a PSR-3 logger (defaulting to Slim\Logger), and produces a PSR-7 ResponseInterface.

Class Signature#

<?php

declare(strict_types=1);

namespace Slim\Handlers;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Slim\Error\Renderers\HtmlErrorRenderer;
use Slim\Error\Renderers\JsonErrorRenderer;
use Slim\Error\Renderers\PlainTextErrorRenderer;
use Slim\Error\Renderers\XmlErrorRenderer;
use Slim\Interfaces\CallableResolverInterface;
use Slim\Interfaces\ErrorHandlerInterface;
use Slim\Interfaces\ErrorRendererInterface;
use Throwable;

class ErrorHandler implements ErrorHandlerInterface
{
    protected string $defaultErrorRendererContentType = 'text/html';

    /** @var ErrorRendererInterface|string|callable */
    protected $defaultErrorRenderer = HtmlErrorRenderer::class;

    /** @var ErrorRendererInterface|string|callable */
    protected $logErrorRenderer = PlainTextErrorRenderer::class;

    /** @var array<string|callable> */
    protected array $errorRenderers = [
        'application/json' => JsonErrorRenderer::class,
        'application/xml' => XmlErrorRenderer::class,
        'text/xml' => XmlErrorRenderer::class,
        'text/html' => HtmlErrorRenderer::class,
        'text/plain' => PlainTextErrorRenderer::class,
    ];

    protected bool $displayErrorDetails = false;
    protected bool $logErrors;
    protected bool $logErrorDetails = false;
    protected ?string $contentType = null;
    protected bool $isContentTypeForced = false;
    protected ?string $method = null;
    protected ServerRequestInterface $request;
    protected Throwable $exception;
    protected int $statusCode;
    protected CallableResolverInterface $callableResolver;
    protected ResponseFactoryInterface $responseFactory;
    protected LoggerInterface $logger;

    public function __construct(
        CallableResolverInterface $callableResolver,
        ResponseFactoryInterface $responseFactory,
        ?LoggerInterface $logger = null
    );

    public function __invoke(
        ServerRequestInterface $request,
        Throwable $exception,
        bool $displayErrorDetails,
        bool $logErrors,
        bool $logErrorDetails
    ): ResponseInterface;

    public function forceContentType(?string $contentType): void;
    public function registerErrorRenderer(string $contentType, $errorRenderer): void;
    public function setDefaultErrorRenderer(string $contentType, $errorRenderer): void;
    public function setLogErrorRenderer($logErrorRenderer): void;

    protected function determineStatusCode(): int;
    protected function determineContentType(ServerRequestInterface $request): ?string;
    protected function determineRenderer(): callable;
    protected function writeToErrorLog(): void;
    protected function logError(string $error): void;
    protected function getDefaultLogger(): LoggerInterface;
    protected function respond(): ResponseInterface;
}

Properties#

Property Visibility Type Default Description
$defaultErrorRendererContentType protected string 'text/html' Fallback Content-type header value when content negotiation does not match a registered MIME type.
$defaultErrorRenderer protected ErrorRendererInterface|string|callable HtmlErrorRenderer::class Fallback renderer resolved via CallableResolverInterface when $contentType is null or unregistered.
$logErrorRenderer protected ErrorRendererInterface|string|callable PlainTextErrorRenderer::class Renderer used by writeToErrorLog() to format the exception string passed to $logger->error().
$errorRenderers protected array<string, ErrorRendererInterface|string|callable> 5 built-in mappings Map of MIME content types (application/json, application/xml, text/xml, text/html, text/plain) to error renderer callables or class names.
$displayErrorDetails protected bool false Whether the HTTP response renderer should include exception type, code, message, file, line, and trace details.
$logErrors protected bool Uninitialized Set on __invoke(); controls whether writeToErrorLog() is executed.
$logErrorDetails protected bool false Set on __invoke(); passed as the $displayErrorDetails flag to $logErrorRenderer.
$contentType protected ?string null Negotiated or forced MIME content type for the current error response.
$isContentTypeForced protected bool false When true, skips determineContentType($request) during __invoke() and retains the forced $contentType.
$method protected ?string null HTTP method extracted from $request->getMethod() during __invoke().
$request protected ServerRequestInterface Uninitialized Most recent PSR-7 server request captured during __invoke().
$exception protected Throwable Uninitialized Caught exception or error captured during __invoke().
$statusCode protected int Uninitialized Resolved HTTP status code (200 for OPTIONS, $exception->getCode() for HttpException, or 500).
$callableResolver protected CallableResolverInterface Constructor Resolves string/container/class error renderers into invokable callables.
$responseFactory protected ResponseFactoryInterface Constructor Creates the base PSR-7 ResponseInterface in respond().
$logger protected LoggerInterface new Logger() PSR-3 logger instance receiving formatted error strings in logError().

Methods & Parameters#

Method Parameters & Types Return Type Description
__construct CallableResolverInterface $callableResolver, ResponseFactoryInterface $responseFactory, ?LoggerInterface $logger = null void Initializes the handler; falls back to getDefaultLogger() (Slim\Logger) if $logger is null.
__invoke ServerRequestInterface $request, Throwable $exception, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails ResponseInterface Captures state, determines status code and content type (unless forced), optionally logs the error, and returns respond().
forceContentType ?string $contentType void Forces a specific MIME type (e.g., 'application/json') for all responses; passing null restores Accept header negotiation.
registerErrorRenderer string $contentType, ErrorRendererInterface|string|callable $errorRenderer void Registers or overrides an error renderer in $errorRenderers[$contentType].
setDefaultErrorRenderer string $contentType, ErrorRendererInterface|string|callable $errorRenderer void Sets both $defaultErrorRendererContentType and $defaultErrorRenderer used when negotiation returns no match.
setLogErrorRenderer ErrorRendererInterface|string|callable $logErrorRenderer void Sets the renderer used by writeToErrorLog() to format log output.
determineStatusCode (none) int Protected. Returns 200 if $this->method === 'OPTIONS', $this->exception->getCode() if instanceof HttpException, else 500.
determineContentType ServerRequestInterface $request ?string Protected. Intersects comma-separated Accept header values with array_keys($this->errorRenderers) (preferring non-text/plain when multiple match) and checks /\+(json|xml)/ vendor media types.
determineRenderer (none) callable Protected. Selects $this->errorRenderers[$this->contentType] if registered, else $this->defaultErrorRenderer, and resolves it via $this->callableResolver->resolve().
writeToErrorLog (none) void Protected. Resolves $this->logErrorRenderer, formats $this->exception with $this->logErrorDetails, appends a tip if using PlainTextErrorRenderer with !$this->displayErrorDetails, and calls logError().
logError string $error void Protected. Delegates directly to $this->logger->error($error).
getDefaultLogger (none) LoggerInterface Protected. Instantiates and returns a new Slim\Logger.
respond (none) ResponseInterface Protected. Creates the response with $this->statusCode, sets Content-type, adds the Allow header if $exception instanceof HttpMethodNotAllowedException, invokes the renderer, and writes the body.

Usage Example#

<?php

use Slim\Error\Renderers\JsonErrorRenderer;
use Slim\Factory\AppFactory;
use Slim\Handlers\ErrorHandler;

$app = AppFactory::create();

$errorHandler = new ErrorHandler(
    $app->getCallableResolver(),
    $app->getResponseFactory()
);

// Force JSON error payloads regardless of the client's Accept header
$errorHandler->forceContentType('application/json');
$errorHandler->setDefaultErrorRenderer('application/json', JsonErrorRenderer::class);

$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler($errorHandler);

Route Callback Invocation Strategies#

When a Route matches an incoming request, it delegates callable execution to an InvocationStrategyInterface. You can set the strategy globally via RouteCollector::setDefaultInvocationStrategy() or per-route via Route::setInvocationStrategy().

05_handlers_and_strategies_2
Route Argument Dispatch Across Slim's Four Invocation Strategies
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.styles import Colors, Styles
from drawlib.text import text

setup(width=180, height=84)

# Left source node: Route::run()
rectangle(
    (26, 42),
    width=40,
    height=34,
    style=Styles.PrimaryFlat.patch(shape_r=2.5),
)
phosphor.git_branch((26, 52.0), width=6.5, style=Styles.White)
text((26, 44.0), "Slim\\Routing\\Route", style=Styles.WhiteBold.patch(text_size=11.5))
text((26, 38.0), "routeArguments =", style=Styles.White.patch(text_size=10.0))
text((26, 32.5), "['id' => '42', 'tab' => 'bio']", style=Styles.WhiteBold.patch(text_size=10.0))

# Strategy 1: RequestResponse (Default)
rectangle(
    (122, 72),
    width=102,
    height=15,
    style=Styles.AccentFlat.patch(shape_r=2.0),
)
text(
    (122, 75.0),
    "1. RequestResponse (Default)  [+ sets request attributes]",
    style=Styles.WhiteBold.patch(text_size=11.0),
)
text(
    (122, 69.0),
    "callable(request, response, ['id' => '42', 'tab' => 'bio'])",
    style=Styles.White.patch(text_size=10.5),
)

# Strategy 2: RequestResponseArgs
rectangle(
    (122, 52),
    width=102,
    height=15,
    style=Styles.PrimaryNeutral.patch(shape_r=2.0),
)
text(
    (122, 55.0),
    "2. RequestResponseArgs  [positional ...array_values(args)]",
    style=Styles.PrimaryBold.patch(text_size=11.0),
)
text(
    (122, 49.0),
    "callable(request, response, '42', 'bio')",
    style=Styles.Dark.patch(text_size=10.5),
)

# Strategy 3: RequestResponseNamedArgs
rectangle(
    (122, 32),
    width=102,
    height=15,
    style=Styles.PrimaryNeutral.patch(shape_r=2.0),
)
text(
    (122, 35.0),
    "3. RequestResponseNamedArgs  [PHP 8+ named ...args]",
    style=Styles.PrimaryBold.patch(text_size=11.0),
)
text(
    (122, 29.0),
    "callable(request, response, id: '42', tab: 'bio')",
    style=Styles.Dark.patch(text_size=10.5),
)

# Strategy 4: RequestHandler
rectangle(
    (122, 12),
    width=102,
    height=15,
    style=Styles.SecondaryNeutral.patch(shape_r=2.0),
)
text(
    (122, 15.0),
    "4. RequestHandler  [PSR-15 style, optional attributes]",
    style=Styles.SecondaryBold.patch(text_size=11.0),
)
text(
    (122, 9.0),
    "callable(request)  -> returns ResponseInterface",
    style=Styles.Dark.patch(text_size=10.5),
)

# Connectors
line((46, 52), (71, 72), arrow_head="->", style=Styles.AccentBold)
line((46, 45), (71, 52), arrow_head="->", style=Styles.PrimaryBold)
line((46, 39), (71, 32), arrow_head="->", style=Styles.PrimaryBold)
line((46, 32), (71, 12), arrow_head="->", style=Styles.SecondaryBold)

save()

classSlim\Handlers\Strategies\RequestResponse#

RequestResponse is Slim 4's default route callback strategy initialized by RouteCollector. Before invoking the route callable, it iterates over $routeArguments and attaches every placeholder key-value pair to the PSR-7 ServerRequestInterface via $request->withAttribute($k, $v). It then invokes the callable with ($request, $response, $routeArguments).

Class Signature#

<?php

declare(strict_types=1);

namespace Slim\Handlers\Strategies;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Interfaces\InvocationStrategyInterface;

class RequestResponse implements InvocationStrategyInterface
{
    /**
     * @param array<string, string> $routeArguments
     */
    public function __invoke(
        callable $callable,
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $routeArguments
    ): ResponseInterface;
}

Properties#

RequestResponse defines no class properties.

Methods & Parameters#

Method Parameters & Types Return Type Description
__invoke callable $callable, ServerRequestInterface $request, ResponseInterface $response, array<string, string> $routeArguments ResponseInterface Copies each $routeArguments entry onto $request via withAttribute($k, $v) and returns $callable($request, $response, $routeArguments).

Usage Example#

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Slim\Handlers\Strategies\RequestResponse;

$app = AppFactory::create();
$app->getRouteCollector()->setDefaultInvocationStrategy(new RequestResponse());

$app->get('/users/{id}', function (Request $request, Response $response, array $args): Response {
    // Accessible via both $args['id'] and $request->getAttribute('id')
    $userId = $args['id'];
    $response->getBody()->write("User ID: {$userId}");
    return $response;
});

classSlim\Handlers\Strategies\RequestResponseArgs#

RequestResponseArgs unpacks route parameters positionally as individual arguments after $request and $response using ...array_values($routeArguments). Unlike RequestResponse, it does not inject route parameters into $request attributes.

Class Signature#

<?php

declare(strict_types=1);

namespace Slim\Handlers\Strategies;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Interfaces\InvocationStrategyInterface;

class RequestResponseArgs implements InvocationStrategyInterface
{
    /**
     * @param array<string, string> $routeArguments
     */
    public function __invoke(
        callable $callable,
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $routeArguments
    ): ResponseInterface;
}

Properties#

RequestResponseArgs defines no class properties.

Methods & Parameters#

Method Parameters & Types Return Type Description
__invoke callable $callable, ServerRequestInterface $request, ResponseInterface $response, array<string, string> $routeArguments ResponseInterface Invokes and returns $callable($request, $response, ...array_values($routeArguments)).

Usage Example#

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Slim\Handlers\Strategies\RequestResponseArgs;

$app = AppFactory::create();
$app->getRouteCollector()->setDefaultInvocationStrategy(new RequestResponseArgs());

$app->get('/orgs/{orgId}/members/{memberId}', function (
    Request $request,
    Response $response,
    string $orgId,
    string $memberId
): Response {
    $response->getBody()->write("Org: {$orgId}, Member: {$memberId}");
    return $response;
});

classSlim\Handlers\Strategies\RequestResponseNamedArgs#

RequestResponseNamedArgs spreads the associative $routeArguments array directly via ...$routeArguments, leveraging PHP 8.0+ named arguments. Route placeholder names in the pattern (e.g., {id}, {format}) must match the callable's parameter names, independent of their parameter order after $request and $response. Like RequestResponseArgs, it does not mutate $request attributes.

Class Signature#

<?php

declare(strict_types=1);

namespace Slim\Handlers\Strategies;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Interfaces\InvocationStrategyInterface;

class RequestResponseNamedArgs implements InvocationStrategyInterface
{
    /**
     * @param array<string, string> $routeArguments
     */
    public function __invoke(
        callable $callable,
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $routeArguments
    ): ResponseInterface;
}

Properties#

RequestResponseNamedArgs defines no class properties.

Methods & Parameters#

Method Parameters & Types Return Type Description
__invoke callable $callable, ServerRequestInterface $request, ResponseInterface $response, array<string, string> $routeArguments ResponseInterface Invokes and returns $callable($request, $response, ...$routeArguments) using PHP named argument unpacking.

Usage Example#

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Slim\Handlers\Strategies\RequestResponseNamedArgs;

$app = AppFactory::create();
$app->getRouteCollector()->setDefaultInvocationStrategy(new RequestResponseNamedArgs());

// Notice that $tab and $id can be declared in any order matching the placeholder names
$app->get('/users/{id}/tabs/{tab}', function (
    Request $request,
    Response $response,
    string $tab,
    string $id
): Response {
    $response->getBody()->write("User {$id} -> Tab {$tab}");
    return $response;
});

classSlim\Handlers\Strategies\RequestHandler#

RequestHandler invokes route callables using the single-argument PSR-15 RequestHandlerInterface signature $callable($request): ResponseInterface, discarding the pre-created $response argument. When constructed with $appendRouteArgumentsToRequestAttributes = true, it copies all $routeArguments key-value pairs into $request attributes prior to calling $callable($request).

Class Signature#

<?php

declare(strict_types=1);

namespace Slim\Handlers\Strategies;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Interfaces\RequestHandlerInvocationStrategyInterface;

class RequestHandler implements RequestHandlerInvocationStrategyInterface
{
    protected bool $appendRouteArgumentsToRequestAttributes;

    public function __construct(bool $appendRouteArgumentsToRequestAttributes = false);

    /**
     * @param array<string, string> $routeArguments
     */
    public function __invoke(
        callable $callable,
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $routeArguments
    ): ResponseInterface;
}

Properties#

Property Visibility Type Default Description
$appendRouteArgumentsToRequestAttributes protected bool false When true, attaches each entry in $routeArguments to $request via $request->withAttribute($k, $v) before invoking $callable($request).

Methods & Parameters#

Method Parameters & Types Return Type Description
__construct bool $appendRouteArgumentsToRequestAttributes = false void Configures whether route parameters are automatically appended to $request attributes.
__invoke callable $callable, ServerRequestInterface $request, ResponseInterface $response, array<string, string> $routeArguments ResponseInterface Optionally injects $routeArguments into $request attributes and returns $callable($request).

Usage Example#

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Slim\Handlers\Strategies\RequestHandler;

$app = AppFactory::create();

// Enable attribute injection so PSR-15 style handlers can read route placeholders
$app->getRouteCollector()->setDefaultInvocationStrategy(new RequestHandler(true));

$app->get('/articles/{slug}', function (Request $request) use ($app): Response {
    $slug = $request->getAttribute('slug');
    $response = $app->getResponseFactory()->createResponse(200);
    $response->getBody()->write("Article: {$slug}");
    return $response;
});