Caution
The documentation you are viewing is for an older version of this component.
Switch to the latest (v2) version.
Reference
Static Resources
One feature of a web server is the ability to serve static files from your filesystem. zend-expressive-swoole provides that capability as well.
To enable this, the package provides an alternate
RequestHandlerRunner
implementation via the class Zend\Expressive\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
Zend\Expressive\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, Zend\Expressive\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.
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 [
'zend-expressive-swoole' => [
'swoole-http-server' => [
'static-files' => [
// 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
Zend\Expressive\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 [
'zend-expressive-swoole' => [
'swoole-http-server' => [
'static-files' => [
'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
Zend\Expressive\Swoole\StaticResourceHandler\MiddlewareInterface
, which
defines the following:
namespace Zend\Expressive\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 Zend\Expressive\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
Zend\Expressive\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
Zend\Expressive\Swoole\StaticResourceHandlerInterface
:
declare(strict_types=1);
namespace Zend\Expressive\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
Zend\Expressive\Swoole\StaticResourceHandlerInterface
to a factory that
returns your custom implementation within your dependencies
configuration.
Found a mistake or want to contribute to the documentation? Edit this page on GitHub!