On this page
Reference
Static Resources
One feature of a web server is the ability to serve static files from your filesystem. mezzio-swoole provides that capability as well.
To enable this, the package provides an alternate
RequestHandlerRunner
implementation via the class Mezzio\Swoole\SwooleRequestHandlerRunner
that performs two duties:
- If a static resource is matched, it serves that.
- Otherwise, it passes off handling to the composed application pipeline.
Internally, the SwooleRequestHandlerRunner
composes another class, a
Mezzio\Swoole\StaticResourceHandlerInterface
instance. This instance
is passed the Swoole request and response, and returns a value indicating
whether or not it was able to identify and serve a matching static resource.
Our default implementation, Mezzio\Swoole\StaticResourceHandler
,
provides an approach that checks an incoming request path against a list of
known extensions, and a configured document root. If the extension matches, it
then checks to see if the file exists in the document root. If it does, it will
serve it.
Disabling static resources
By default, we ship with static resource handling enabled.
This is done by having the Mezzio\Swoole\Event\StaticResourceRequestListener
in the list of listeners provided for the Mezzio\Swoole\Event\RequestEvent
.
To disable that listener, you will need to replace the set of listeners for that event, to include only the Mezzio\Swoole\Event\RequestHandlerRequestListener
.
You can do that in your application configuration as follows:
// in config/autoload/dependencies.global.php:
use Laminas\Stdlib\ArrayUtils\MergeReplaceKey;
use Mezzio\Swoole\Event;
return [
// ...
'mezzio-swoole' => [
// ...
'swoole-http-server' => [
// ...
'listeners' => [
Event\RequestEvent::class => new MergeReplaceKey([
Event\RequestHandlerRequestListener::class,
]),
],
],
],
// ...
];
Middleware
The StaticResourceHandler
implementation performs its work by composing a
queue of middleware to execute when attempting to serve a matched file. Using
this approach, we are able to provide a configurable set of capabilities for
serving static resources. What we currently provide is as follows:
-
CacheControlMiddleware
will set aCache-Control
header based on configuration you provide it. Configuration uses a combination of regular expressions to match against the path, with theCache-Control
directive to use when the match occurs. -
ClearStatCacheMiddleware
will, if configured to do so, callclearstatcache()
either on every request, or at specific intervals. This is useful if you anticipate filesystem changes in your document root. -
ContentTypeFilterMiddleware
checks the incoming filename against a map of known extensions and their associated Content-Type values. If it cannot match the file, it returns a value indicating no match was found so that the application can continue processing the request. Otherwise, it provides the Content-Type for the associated response. This middleware is generally best used as the outermost layer, to ensure no other middleware executes in the case that the file cannot be matched. -
ETagMiddleware
will set anETag
header using either a strong or weak algorithm, and only on files matching given regular expressions. If theETag
header value matches either anIf-Match
orIf-None-Match
request header, it will provide a response status of304
and disable sending content. -
GzipMiddleware
detects theAccept-Encoding
request header and, if present, and the compression level provided to the instance allows, it will compress the returned response content using either gzip or deflate compression as requested. -
HeadMiddleware
will force an empty response. (The status and headers may be set by other middleware.) -
LastModifiedMiddleware
will set aLast-Modified
header using thefilemtime()
value of the requested resource. If the header value is later than anIf-Modified-Since
request header, it will provide a response status of304
and disable sending content. -
MethodNotAllowedMiddleware
will set the response status to405
, and set anAllow
header indicating the allowed methods when an unsupported request method is provided. -
OptionsMiddleware
will force an empty response with anAllow
header set to the allowed methods. (Other headers may also be present!)
By default, these are registered in the following order, contingent on configuration being provided:
ContentTypeFilterMiddleware
MethodNotAllowedMiddleware
OptionsMiddleware
HeadMiddleware
GzipMiddleware
ClearStatCacheMiddleware
CacheControlMiddleware
LastModifiedMiddleware
ETagMiddleware
This approach ensures that the most expensive operations are never called unless
other conditions are met (e.g., if the HTTP request method is not allowed,
there's no need to calculate the Last-Modified
or ETag
headers); it also
ensures that all possible headers are provided whenever possible (e.g., a HEAD
request should also expose Cache-Control
, Last-Modified
, and ETag
headers).
Providing your own middleware
If you want to disable middleware, or to provide an alternate list of middleware (including your own!), you will need to provide an alternate
StaticResourceHandler
factory. In most cases, you can extendStaticResourceHandlerFactory
and override theconfigureMiddleware(array $config) : array
method to do so. Be sure to remember to add adependencies
setting mapping theStaticResourceHandlerInterface
service to your new factory when done!
Configuration
We provide a factory for the StaticResourceHandler
that uses a
configuration-driven approach in order to:
- Set the document root.
- Set the map of allowed extensions to content-types.
- Configure and provide middleware.
The following demonstrates all currently available configuration options:
// config/autoload/swoole.local.php
return [
'mezzio-swoole' => [
'swoole-http-server' => [
'static-files' => [
// Since 2.1.0: Set to false to disable any serving of static
// files; all other configuration will then be ignored.
'enable' => true,
// Document root; defaults to "getcwd() . '/public'"
'document-root' => '/path/to/static/files/to/serve',
// Extension => content-type map.
// Keys are the extensions to map (minus any leading `.`),
// values are the MIME type to use when serving them.
// A default list exists if none is provided.
'type-map' => [],
// How often a worker should clear the filesystem stat cache.
// If not provided, it will never clear it. The value should be
// an integer indicating the number of seconds between clear
// operations. 0 or negative values will clear on every request.
'clearstatcache-interval' => 3600,
// Which ETag algorithm to use.
// Must be one of "weak" or "strong"; the default, when none is
// provided, is "weak".
'etag-type' => 'weak|strong',
// gzip options
'gzip' => [
// Compression level to use.
// Should be an integer between 1 and 9; values less than 1
// disable compression.
'level' => 4,
],
// Rules governing which server-side caching headers are emitted.
// Each key must be a valid regular expression, and should match
// typically only file extensions, but potentially full paths.
// When a static resource matches, all associated rules will apply.
'directives' => [
'regex' => [
'cache-control' => [
// one or more valid Cache-Control directives:
// - must-revalidate
// - no-cache
// - no-store
// - no-transform
// - public
// - private
// - max-age=\d+
],
'last-modified' => bool, // Emit a Last-Modified header?
'etag' => bool, // Emit an ETag header?
],
],
],
],
],
];
Security warning
Never add
php
as an allowed static file extension, as doing so could expose the source code of your PHP application!Document root
If no
document_root
configuration is present, the default is to usegetcwd() . '/public'
. If either the configured or default document root does not exist, we raise an exception.Default extension/content-types
By default, we serve files with extensions in the whitelist defined in the constant
Mezzio\Swoole\StaticResourceHandler\ContentTypeFilterMiddleware::DEFAULT_STATIC_EXTS
, which is derived from a list of common web MIME types maintained by Mozilla.
Configuration Example
The example which follows provides the following options:
- Sets the document root to
/var/www/htdocs
. - Adds a custom extension / content-type map.
- Provides a clearstatcache interval of 2 hours.
- Selects the "strong" ETag algorithm.
- Indicates a gzip compression level of 3.
- Sets Cache-Control, Last-Modified, and ETag directives for JS, CSS, and image files.
- Sets Cache-Control directives for plain text files.
// config/autoload/swoole.local.php
return [
'mezzio-swoole' => [
'swoole-http-server' => [
'static-files' => [
'enable' => true,
'document-root' => '/var/www/htdocs',
'type-map' => [
'css' => 'text/css',
'gif' => 'image/gif',
'ico' => 'image/x-icon',
'jpg' => 'image/jpg',
'jpeg' => 'image/jpg',
'js' => 'application/javascript',
'png' => 'image/png',
'svg' => 'image/svg+xml',
'txt' => 'text/plain',
],
'clearstatcache-interval' => 7200,
'etag-type' => 'strong',
'gzip' => [
'level' => 3,
],
'directives' => [
'/\.(css|gif|ico|jpg|jpeg|png|svg|js)$/' => [
'cache-control' => [
'public',
'no-transform',
],
'last-modified' => true,
'etag' => true,
],
'/\.txt$/' => [
'cache-control' => [
'public',
'no-cache',
],
],
],
],
],
],
];
Writing Middleware
Static resource middleware must implement
Mezzio\Swoole\StaticResourceHandler\MiddlewareInterface
, which
defines the following:
namespace Mezzio\Swoole\StaticResourceHandler;
use Swoole\Http\Request;
interface MiddlewareInterface
{
/**
* @param string $filename The discovered filename being returned.
* @param callable $next has the signature:
* function (Request $request, string $filename) : StaticResourceResponse
*/
public function __invoke(
Request $request,
string $filename,
callable $next
) : StaticResourceResponse;
}
The $next
argument has the following signature:
namespace Mezzio\Swoole\StaticResourceHandler;
use Swoole\Http\Request;
public function __invoke(
Request $request,
string $filename
) : StaticResourceResponse;
Typically, middleware will look something like this:
$response = $next($request, $filename);
// if some request condition does not match:
// return $response;
// Otherwise, manipulate the returned $response instance and then return it.
Middleware either produces or manipulates a
Mezzio\Swoole\StaticResourceHandler\StaticResourceResponse
instance.
That class looks like the following:
class StaticResourceResponse
{
/**
* @param callable $responseContentCallback Callback to use when emitting
* the response body content via Swoole. Must have the signature:
* function (SwooleHttpResponse $response, string $filename) : void
*/
public function __construct(
int $status = 200,
array $headers = [],
bool $sendContent = true,
callable $responseContentCallback = null
);
public function addHeader(string $name, string $value) : void;
public function disableContent() : void;
/**
* Call this method to indicate that the request cannot be served as a
* static resource. The request runner will then proceed to execute
* the associated application in order to generate the response.
*/
public function markAsFailure() : void;
/**
* @param callable $responseContentCallback Callback to use when emitting
* the response body content via Swoole. Must have the signature:
* function (SwooleHttpResponse $response, string $filename) : void
*/
public function setResponseContentCallback(callable $callback) : void;
/**
* Use this within a response content callback to set the associated
* Content-Length of the generated response. Loggers can then query
* for this information in order to provide that information in the logs.
*/
public function setContentLength(int $length) : void;
public function setStatus(int $status) : void;
}
Most middleware will conditionally set the status, one or more headers, and
potentially disable returning the response body (via disableContent()
).
Middleware that restricts access or filters out specific files will also use
markAsFailure()
.
Providing an alternative mechanism for sending response content
In some cases, you may want to alter how the
Swoole\Http\Response
receives the body content. By default, we useSwoole\Http\Response::sendfile()
. However, this may not work well when performing tasks such as compression, appending a watermark, etc. As an example, theGzipMiddleware
adds a compression filter to a filehandle representing the file to send, and then callsSwoole\Http\Response::write()
in a loop until all content is sent.To perform work like this, you can call the
StaticResourceResponse::setResponseContentCallback()
method as detailed in the section above within your middleware.
Alternative static resource handlers
As noted at the beginning of this chapter, the SwooleRequestHandlerRunner
composes a StaticResourceHandlerInterface
instance in order to determine if a
resource was matched by the request, and then to serve it.
If you want to provide an alternative mechanism for doing so (e.g., to serve
files out of a caching server), you will need to implement
Mezzio\Swoole\StaticResourceHandlerInterface
:
declare(strict_types=1);
namespace Mezzio\Swoole;
use Swoole\Http\Request as SwooleHttpRequest;
use Swoole\Http\Response as SwooleHttpResponse;
interface StaticResourceHandlerInterface
{
/**
* Attempt to process a static resource based on the current request.
*
* If the resource cannot be processed, the method should return null.
* Otherwise, it should return the StaticResourceResponse that was used
* to send the Swoole response instance. The runner can then query this
* for content length and status.
*/
public function processStaticResource(
SwooleHttpRequest $request,
SwooleHttpResponse $response
) : ?StaticResourceHandler\StaticResourceResponse;
}
Once implemented, map the service
Mezzio\Swoole\StaticResourceHandlerInterface
to a factory that
returns your custom implementation within your dependencies
configuration.
Example alternate static resource handler: StaticMappedResourceHandler
- Since 2.7.0
The default static resource handler, Mezzio\Swoole\StaticResourceHandler
, requires all files to be in the specified document root directory (by default, "public") when instantiating the handler.
If you are using modules generating templates with associated file assets (JavaScript, CSS, etc.), those files must be copied to the "public" directory if you wish to allow access to them.
This can be done via scripting, but is one more step to consider when testing or deploying a site.
Ideally, a module should be able to contain both its template and any dependencies that template relies upon.
For example, assume you have a module, AwesomeModule, with a handler called "HomeHandler", which renders the 'home' template.
You designate the prefix, /awesome-home
for rendering the assets.
The structure of your module files looks like this:
AwesomeModule
├── src
| ├── Handler
| | ├── HomeHandler.php
| | ├── HomeHandlerFactory.php
| ├── ConfigProvider.php
├── templates
│ ├── home
| | ├── home.html
| | ├── style.css
│ ├── layouts
In your home.html
template, you can refer to the style.css
file, using /awesome-home
as follows:
<link href="/awesome-home/style.css" rel="stylesheet" type="text/css">
Currently, however, this will cause errors, as the stylesheet will not be available under the public/
tree of the application.
You would need to copy or symlink the files to the appropriate location to make that work.
The StaticMappedResourceHandler
solves this problem.
Using StaticMappedResourceHandler
To use Mezzio\Swoole\StaticMappedResourceHandler
from an application or module:
- Define what your URI prefix will be (e.g.,
/awesome-home
). - Update references to linkable resources in your templates to use the desired prefix (e.g.,
<script src='/awesome-home/style.css'></script>
). - In your application/module configuration (or
ConfigProvider
), add the relationship between your prefix (awesome-home
) and any directories containing the assets. - In the application's configuration, set the alias of
Mezzio\Swoole\StaticResourceHandlerInterface
to useMezzio\Swoole\StaticMappedResourceHandler
.
For step #3, in your module's ConfigProvider, you can add a configuration setting as follows:
public function __invoke() : array
{
return [
'config' => [
'mezzio-swoole' => [
'swoole-http-server' => [
'static-files' => [
'mapped-document-roots' => [
'awesome-home' => __DIR__ . '/../../templates/home'
]
]
]
]
]
];
}
Note that prefixes are always the first part after the host, and specifying the initial slash is optional (i.e. awsesome-home
and /awesome-home
both work and represent the same thing).
In step #4, in your application's configuration (autoload/dependencies.global.php
is a good place), override the default implementation of StaticResourceHandlerInterface
:
return [
'dependencies' => [
'aliases' => [
Mezzio\Swoole\StaticResourceHandlerInterface::class => Mezzio\Swoole\StaticMappedResourceHandler::class
// Fully\Qualified\ClassOrInterfaceName::class => Fully\Qualified\ClassName::class,
],
// etc.
For step #3, an alternative to storing a configuration is dynamically associating /awesome-home
to a directory in code (probably within a factory).
This approach could be useful if the directory of the assets isn't know until runtime.
use Psr\Container\ContainerInterface;
use Mezzio\Template\TemplateRendererInterface;
use Mezzio\Swoole\StaticResourceHandler\FileLocationRepositoryInterface;
class AwesomeHomeHandlerFactory
{
public function __invoke(ContainerInterface $container) : DocumentationViewHandler
{
// Establish location for the home template assets
$repo = $container->get(FileLocationRepositoryInterface::class);
$repo->addMappedDocumentRoot(
'awesome-home',
realpath(__DIR__ . '/../../templates/home')
);
return new AwesomeHomeHandler(
$container->get(TemplateRendererInterface::class)
);
}
}
When the template renders, the client will request /awesome-home/style.css
, which the StaticMappedResourceHandler
will now retrieve from the templates/home/
folder of the module.
Mezzio\Swoole\StaticMappedResourceHandler
uses the Mezzio\Swoole\StaticResourceHandler\FileLocationRepository
(which implements Mezzio\Swoole\StaticResourceHandler\FileLocationRepositoryInterface
) to maintain an association of URI prefixes with file directories.
If you require using a file location that requires authentication, decompression, etc. you can override the default functionality by creating your own implementation of Mezzio\Swoole\StaticResourceHandler\FileLocationRepositoryInterface
.