interitty/static-content-generator

Static content generator persists application output for future use, saving time and money.

v1.0.5 2024-09-01 12:01 UTC

This package is auto-updated.

Last update: 2024-10-31 10:16:49 UTC


README

Static content generator persists application output for future use, saving time and money.

Requirements

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 parameterDescription
autoEndWhen processBegin was called, catch all relevant content into the storage automatically? The default value is "false"
autoStartCall processBegin before every request? The default value is "false"
basePathPrefix for all generated file paths in the processFileName helper. The default value is "/"
destinationDestination folder to write the generated static content. The default value is "%wwwDir%"
productionModeProduction mode suppresses a possible exception when writing to the read-only filesystem and creates a log entry
readOnlyFlag if the file system is in read-only mode. The default value null is used for autodetection by path in the destination parameter
writeFlagsUsed 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