interitty / static-content-generator
Static content generator persists application output for future use, saving time and money.
Requires
- php: ~8.3
- dg/composer-cleaner: ~2.2
- interitty/output-buffer-manager: ~1.0
- interitty/utils: ~1.0
- league/flysystem: ~3.29
- psr/log: ~3.0
Requires (Dev)
- interitty/application: ~1.0
- interitty/code-checker: ~1.0
- interitty/di: ~1.0
- interitty/phpunit: ~1.0
README
Static content generator persists application output for future use, saving time and money.
Requirements
- PHP >= 8.3
Installation
The best way to install interitty/static-content-generator is using Composer:
composer require interitty/static-content-generator
Then register the extension in the Nette config file:
# app/config/config.neon
extensions:
staticContentGenerator: Interitty\StaticContentGenerator\Nette\DI\StaticContentGeneratorExtension
Settings
There are some more settings, you can need to fit your suit.
Neon parameter | Description |
---|---|
autoEnd | When processBegin was called, catch all relevant content into the storage automatically? The default value is "false " |
autoStart | Call processBegin before every request? The default value is "false " |
basePath | Prefix for all generated file paths in the processFileName helper. The default value is "/ " |
destination | Destination folder to write the generated static content. The default value is "%wwwDir% " |
productionMode | Production mode suppresses a possible exception when writing to the read-only filesystem and creates a log entry |
readOnly | Flag if the file system is in read-only mode. The default value null is used for autodetection by path in the destination parameter |
writeFlags | Used file_put_contents flags. The default value is "LOCK_EX " |
Usage
For comfortable work with the Static content generator, there is a Nette DI extension, that registers all required services for future work. It also allows registering into the Nette lifecycle to automatically persist all relevant static content into the specified folder.
Example: catch all relevant static content automatically
This configuration catches all the responses with HTTP status 200: OK
that is allowed to be cached (it means,
that HTTP header Cache-Control
or Pragma
does not contain "no-cache
" or "no-store
" value).
extensions:
staticContentGenerator: Interitty\StaticContentGenerator\Nette\DI\StaticContentGeneratorExtension
staticContentGenerator:
autoStart: true
destination: %wwwDir%
Example: catch specific static content automatically
There can be many cases, where it could be useful to set up, which Presenter:action
should be stored. For this occasion, there is autoEnd
parameter, which means to catch all relevant responses every time
the processBegin
was called manually.
extensions:
staticContentGenerator: Interitty\StaticContentGenerator\Nette\DI\StaticContentGeneratorExtension
staticContentGenerator:
autoStart: false # Default behavior
autoEnd: true
The processBegin
was not called automatically, because the autoStart
parameter is false
by default. It allows
having any sophisticated condition, to specify, which page should be persisted
class CmdPresenter extends \Nette\Application\UI\Presenter
{
/** @var \Interitty\StaticContentGenerator\StaticContentGenerator @inject */
protected $generator;
public function actionDefault($page)
{
if($page !== 'Admin') {
$this->staticContentGenerator->processBegin();
}
}
}
Partial usage
The StaticContentGenerator
class can simply catch the output content and persist them into the given storage.
Thanks to the FlySystem, the generator can store almost anywhere
locally, on remote FTP,
or cloud storage like AWS S3 or Azure.
Because of a need to persist more files with their contents at once, like in the cache warmup process, each part is
separated by the StaticContentHandler
class.
Example: catch content into the file by the StaticContentHandler
The following example catches any content that was sent to the output and stores them in the file specified by the given
$filename
variable. Because the optional parameter $muteOutput
is specified as false
, everything will be
also sent to the output or another registered Output buffer handler.
// Flysystem setup
$destination = sys_get_temp_dir();
$adapter = new \League\Flysystem\Adapter\Local($destination);
$filesystem = new \League\Flysystem\Filesystem($adapter);
// OutputBufferManager setup
$outputBufferManager = new \Interitty\OutputBufferManager\OutputBufferManager();
$filename = 'data.txt';
$muteOutput = false;
$storage = new \Interitty\StaticContentGenerator\Storage\FilesystemStorage($filesystem);
$handler = new \Interitty\StaticContentGenerator\Handler\StaticContentHandler($storage, $filename, $muteOutput);
$outputBufferManager->begin('…', [$handler, 'processManageOutput']);
$handler->processBegin();
// Any content that was sent to the output
echo 'testContent';
$outputBufferManager->end('…');
$handler->processEnd();
Example: cache warmup by the StaticContentGenerator
The following example simulates the cache warmup process, which catches any content that was sent to the output
and stores them into the files specified by the given $filename
variable. Because the optional parameter $muteOutput
is specified as true
, nothing will be sent to the output or another registered
Output buffer handler.
// Test data
$testData = [
'test1.txt' => 'Test 1',
'test2.txt' => 'Test 2',
];
// Flysystem setup
$destination = __DIR__;
$adapter = new \League\Flysystem\Adapter\Local($destination);
$filesystem = new \League\Flysystem\Filesystem($adapter);
// OutputBufferManager setup
$muteOutput = true;
$outputBufferManager = new \Interitty\OutputBufferManager\OutputBufferManager();
$storage = new \Interitty\StaticContentGenerator\Storage\FilesystemStorage($filesystem);
$generator = new \Interitty\StaticContentGenerator\StaticContentGenerator($outputBufferManager, $storage);
foreach ($testData as $filename => $content) {
$handler = $generator->createHandler($filename, $muteOutput);
$generator->setHandler($handler);
$generator->processBegin();
// Any content that was sent to the output
echo $content;
$generator->processEnd();
}
Example: cache warmup from a list of URLs
The cache warmup process is mostly based on the list of URLs. Because of that, the StaticContentGenerator
contains
the processFileName()
helper method that detects the sdestination filename from the path part of the given URL.
// Test data
$testData = [
'/' => 'Index test',
'/test.html' => 'Test',
'/test' => 'Subfolder test',
];
// Flysystem setup
$destination = __DIR__;
$adapter = new \League\Flysystem\Adapter\Local($destination);
$filesystem = new \League\Flysystem\Filesystem($adapter);
// OutputBufferManager setup
$muteOutput = true;
$outputBufferManager = new \Interitty\OutputBufferManager\OutputBufferManager();
$storage = new \Interitty\StaticContentGenerator\Storage\FilesystemStorage($filesystem);
$generator = new \Interitty\StaticContentGenerator\StaticContentGenerator($outputBufferManager, $storage);
foreach ($testData as $path => $content) {
$filename = $generator->processFileName($path);
$handler = $generator->createHandler($filename, $muteOutput);
$generator->setHandler($handler);
$generator->processBegin();
// Any content that was sent to the output
echo $content;
$generator->processEnd();
}
Example: catch Request/Response content into a file by the MiddlewareStaticContentGenerator
It can be useful to catch the content of the HTTP response for the incoming request.
// Request/Response setup
$url = 'http://localhost/';
$urlScript = new \Nette\Http\UrlScript($url);
$request = new \Nette\Http\Request($urlScript);
$response = new \Nette\Http\Response();
$response->setCode(\Nette\Http\IResponse::S200_OK);
// Flysystem setup
$destination = __DIR__;
$adapter = new \League\Flysystem\Adapter\Local($destination);
$filesystem = new \League\Flysystem\Filesystem($adapter);
// OutputBufferManager setup
$outputBufferManager = new \Interitty\OutputBufferManager\OutputBufferManager();
$storage = new \Interitty\StaticContentGenerator\Storage\FilesystemStorage($filesystem);
$generator = new \Interitty\StaticContentGenerator\Nette\MiddlewareStaticContentGenerator($request, $response, $outputBufferManager, $storage);
$generator->processBegin();
// Any content that was sent to the output
echo 'testContent';
$generator->processEnd();
Read-only filesystem (Docker support)
If the application is packaged in a docker container, it is advisable to run it in read-only mode in the production environment.
In this mode, all static assets should already be pre-generated and used by the web server (apache, nginx, …), which should result in the PHP application not being called at all. So if it does get called, it can be considered a deficiency and is logged as such.
By default, this mode is automatically detected based on the folder permissions specified by the destination
parameter
combined with the productionMode
flag. In a development environment, the read-only mode does not make much sense, but
if it is still active, the exception is not caught to resolve the situation as soon as possible. Both of these parameters
can be adjusted in the configuration
extensions:
staticContentGenerator: Interitty\StaticContentGenerator\Nette\DI\StaticContentGeneratorExtension
staticContentGenerator:
productionMode: %productionMode% # Default behavior
readOnly: null # Default behavior