Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.
API Index — Namespaces, Packages, Reports, Indices

ExpiresMiddleware
in package
implements MiddlewareInterface

Expires Middleware (Apache mod_expires equivalent)

Stamps an Expires: header on the response based on its Content-Type. Modern clients prefer Cache-Control: max-age (see CacheControlMiddleware) but many legacy proxies and browsers still honour Expires, and the Apache ExpiresByType image/jpeg "access plus 1 month" idiom is so common in migrated .htaccess files that the name parity matters.

Apache equivalent:

ExpiresActive On
ExpiresDefault                    "access plus 5 minutes"
ExpiresByType image/jpeg          "access plus 1 month"
ExpiresByType text/css            "access plus 1 year"

Constructor takes a Content-Type prefix => relative-date map. Values are parsed by strtotime() so any of these forms work: '+1 year', '+30 days', '+5 minutes', '+86400 seconds'

Base time ('A' vs 'M'): Pass base: 'A' (default) to compute expiry relative to the current request time — Apache ExpiresDefault "access plus N" / A base. Pass base: 'M' to compute expiry relative to the Last-Modified header value on the response (file modification time) — Apache M base. If no Last-Modified header is present on the response, the middleware falls back to access-time (the current request time) and the behaviour is identical to base: 'A'.

Dual-header emission (emitCacheControl: true): Apache's set_expiration_fields() always emits both Expires: and Cache-Control: max-age=N from a single config rule, where max-age is derived as expires - request_time (mod_expires.c:432–437). Set $emitCacheControl = true to replicate this atomicity: when Expires is stamped, a matching Cache-Control: max-age=N, public (or private) is also emitted with the same delta, keeping both headers in sync to the second. Existing Cache-Control headers are not overwritten (same skip-if-present guard as CacheControlMiddleware).

Error-response suppression: Apache mod_expires never stamps headers on 4xx/5xx responses (mod_expires.c:455–458). This middleware follows the same rule: responses with status >= 400 are returned unchanged.

Negative/past expiry clamping: Apache clamps past expiry to request_time (mod_expires.c:429–431), which results in max-age=0. This middleware does the same: if the computed expiry timestamp is in the past, Expires is set to the current time and Cache-Control: max-age=0 is emitted (when $emitCacheControl = true).

Usage in app.php:

// Access-time base, Expires header only (legacy compat):
$app->addMiddleware(new \ZealPHP\Middleware\ExpiresMiddleware(
    byType: [
        'image/'           => '+30 days',
        'text/css'         => '+1 year',
        'text/javascript'  => '+1 year',
        'font/'            => '+1 year',
    ],
    default: '+5 minutes',
));

// Apache-parity: both Expires + Cache-Control: max-age from one rule:
$app->addMiddleware(new \ZealPHP\Middleware\ExpiresMiddleware(
    byType: ['image/' => '+30 days', 'text/css' => '+1 year'],
    default: '+5 minutes',
    emitCacheControl: true,
));

// M (modification-time) base — expiry relative to Last-Modified:
$app->addMiddleware(new \ZealPHP\Middleware\ExpiresMiddleware(
    byType: ['text/html' => '+5 minutes'],
    base: 'M',
    emitCacheControl: true,
));

Match is by Content-Type prefix (first match wins, longest-prefix-first for determinism). default applies when no prefix matches; pass null to skip stamping when nothing matches (Apache ExpiresDefault unset).

Table of Contents

Interfaces

MiddlewareInterface

Properties

$base  : string
$byType  : array<string, string>
$default  : string|null
$emitCacheControl  : bool
$publicCache  : bool

Methods

__construct()  : mixed
process()  : ResponseInterface
resolveRelative()  : string|null

Properties

Methods

__construct()

public __construct([array<string, string> $byType = [] ][, string|null $default = null ][, string $base = 'A' ][, bool $emitCacheControl = false ][, bool $publicCache = true ]) : mixed
Parameters
$byType : array<string, string> = []

CT-prefix => relative-date

$default : string|null = null

Relative-date for unmatched CTs (null = skip)

$base : string = 'A'

'A' = access/request time (default); 'M' = Last-Modified mtime

$emitCacheControl : bool = false

Also emit Cache-Control: max-age=N (Apache dual-header parity)

$publicCache : bool = true

Whether emitted Cache-Control uses 'public' (true) or 'private'

process()

public process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
Parameters
$request : ServerRequestInterface
$handler : RequestHandlerInterface
Return values
ResponseInterface

resolveRelative()

private resolveRelative(string $ct) : string|null
Parameters
$ct : string
Return values
string|null
On this page