ampproject / amp-toolbox
A collection of AMP tools making it easier to publish and host AMP pages with PHP.
Installs: 532 441
Dependents: 1
Suggesters: 0
Security: 0
Stars: 73
Watchers: 11
Forks: 25
Open Issues: 39
Requires (Dev)
- ext-zip: *
- civicrm/composer-downloads-plugin: ^2.1 || ^3.0
- dealerdirect/phpcodesniffer-composer-installer: ^0.7.1 || ^1.0.0
- mikey179/vfsstream: ^1.6
- php-parallel-lint/php-parallel-lint: ^1.2
- phpcompatibility/php-compatibility: ^9
- phpunit/phpunit: ^5 || ^6 || ^7 || ^8 || ^9
- roave/security-advisories: dev-master
- sirbrillig/phpcs-variable-analysis: ^2.11.2
- squizlabs/php_codesniffer: ^3
- wp-coding-standards/wpcs: ^2.3
- yoast/phpunit-polyfills: ^0.2.0 || ^1.0.0
Suggests
- ext-json: Provides native implementation of json_encode()/json_decode().
- ext-mbstring: Used by Dom\Document to convert encoding to UTF-8 if needed.
- mck89/peast: Needed to minify the AMP script.
- nette/php-generator: Needed to generate the validator spec PHP classes and interfaces.
- dev-main
- 0.11.6
- 0.11.5
- 0.11.4
- 0.11.3
- 0.11.2
- 0.11.1
- 0.11.0
- 0.10.0
- 0.9.3
- 0.9.2
- 0.9.1
- 0.9.0
- 0.8.2
- 0.8.1
- 0.8.0
- 0.7.0
- v0.6.0
- 0.5.2
- 0.5.1
- 0.5.0
- 0.4.0
- 0.3.0
- 0.2.0
- 0.1.1
- 0.1.0
- dev-sync-local-fallback-files
- dev-dependabot/composer/civicrm/composer-downloads-plugin-tw-2.1or-tw-3.0or-tw-4.0
- dev-dependabot/composer/wp-coding-standards/wpcs-tw-2.3or-tw-3.0
- dev-dependabot/composer/yoast/phpunit-polyfills-tw-0.2.0or-tw-1.0.0or-tw-3.0.0
- dev-add/ecs
This package is auto-updated.
Last update: 2024-12-15 00:22:41 UTC
README
AMP Toolbox for PHP
A collection of AMP tools making it easier to publish and host AMP pages with PHP.
The following tools are part of this library:
AMP Optimizer for PHP
AMP Optimizer is a library for doing server-side optimization to AMP markup by automatically applying AMP performance best practices and enabling AMP server-side-rendering.
Table of Contents (click to expand)
Conceptual Overview
The AMP Optimizer is a AmpProject\Optimizer\TransformationEngine
object that sets up a pipeline of consecutive AmpProject\Optimizer\Transformer
objects. The engine takes unoptimized input in the form of either a HTML markup string or an AmpProject\Dom\Document
object and turns it into an optimized HTML markup string.
During the process, errors might occur that make parts of the optimization impossible. These are collected within an AmpProject\Optimizer\ErrorCollection
object that you can then iterate over to find out more and provide feedback as needed.
Installation
The AMP Optimizer is part of the AMP Toolbox for PHP library that you can pull into your project via the Composer PHP package manager:
composer require ampproject/amp-toolbox
Basic Usage
The following code snippet shows the most basic way of using the AMP Optimizer:
use AmpProject\Optimizer\ErrorCollection; use AmpProject\Optimizer\TransformationEngine; $transformationEngine = new TransformationEngine(); // 1. $errorCollection = new ErrorCollection; // 2. $optimizedHtml = $transformationEngine->optimizeHtml( // 3. $unoptimizedHtml, // 4. $errorCollection // 5. );
- First we instantiate the transformation engine itself.
- Then we instantiate an
AmpProject\Optimizer\ErrorCollection
object as we need a "bag" to collect the errors in and pass them around. - As a final step, we store the result of calling the transformation engine's
optimizeHtml()
method, which requires... - ... the unoptimized input HTML markup as a string and ...
- ... the empty error collection we've already instantiated. After the transformation engine ran, this collection will contain all errors that were encountered during the transformation.
Usage with a DOM Representation
If you already have a DOM representation, there's no need to save it as HTML first to use it with the transformation engine. The transformation engine accepts an AmpProject\Dom\Document
object[^1] directly via its optimizeDom()
method.
If you have a regular PHP built-in DOMDocument
instead, you can turn it into an AmpProject\Dom\Document
using AmpProject\Dom\Document::fromNode()
.
use AmpProject\Dom\Document; use AmpProject\Optimizer\ErrorCollection; use AmpProject\Optimizer\TransformationEngine; if (! $dom instanceof Document) { $dom = Document::fromNode($dom); } $transformationEngine = new TransformationEngine(); $errorCollection = new ErrorCollection; $transformationEngine->optimizeDom($dom, $errorCollection);
Do note that the optimizeDom()
doesn't have a return value, as it changes the provided AmpProject\Dom\Document
in-place.
Working with Errors
The AmpProject\Optimizer\ErrorCollection
that you pass into the transformation engine's optimizeHtml()
or optimizeDom()
method should ideally stay empty after the optimization pass.
To check whether errors were found, you can iterate over the collection, which will provide you with 0 or more AmpProject\Optimizer\Error
objects.
$errorCollection = new ErrorCollection; // Do the transformation here, while passing in the $errorCollection object. foreach ($errorCollection as $error) { printf( "Error code: %s\nError Message: %s\n", $error->getCode(), $error->getMessage() ); }
A quick count of the errors can be done for early returns as needed:
if ($errorCollection->count() > 0) { $this->log('The AMP serverside optimization process produced one or more errors.'); }
You can check whether the collection of errors contains an error with a specific code as well. The current convention is that all errors have their class shortname (the class name without the namespace) as the error code.
if ($errorCollection->has('CannotRemoveBoilerplate')) { $this->log('The boilerplate was not removed by the Optimizer.'); }
Note that this only lets you check whether an error "category" popped up. It can be one or more errors with that same code. If you need a more detailed check, you should iterate over the collection instead.
Included Transformers
Configuring the Transformers
You can inject a configuration object into the AmpProject\Optimizer\TransformationEngine
to override the default configuration.
The AmpProject\Optimizer\Configuration
interface and its default implementation AmpProject\Optimizer\DefaultConfiguration
will provide the list of transformers to use, as well as give access to child objects they store that are Transformer-specific configuration objects.
To override the list of transformers to use, you can provide an array containing the AmpProject\Optimizer\Configuration::KEY_TRANSFORMERS
key.
use AmpProject\Optimizer\Configuration; use AmpProject\Optimizer\DefaultConfiguration; use AmpProject\Optimizer\TransformationEngine; use AmpProject\Optimizer\Transformer; $configurationData = [ Configuration::KEY_TRANSFORMERS => [ Transformer\ServerSideRendering::class, Transformer\AmpRuntimeCss::class, Transformer\TransformedIdentifier::class, ], ]; $transformationEngine = new TransformationEngine( new DefaultConfiguration($configurationData) );
Configuration values for the transformers can be stored under the fully qualified class name of these transformers. This can be easily done by using their ::class
constant.
They will also usually provide publicly accessible constants for the known configuration keys as well.
In the following example, we configure the AmpProject\Optimizer\Transformer\AmpRuntimeCss
transformer by setting its 'canary'
option to true
(which would default to false).
use AmpProject\Optimizer\Configuration; use AmpProject\Optimizer\DefaultConfiguration; use AmpProject\Optimizer\TransformationEngine; use AmpProject\Optimizer\Transformer; $configurationData = [ Transformer\AmpRuntimeCss::class => [ Configuration\AmpRuntimeCssConfiguration::CANARY => true, ], ]; $transformationEngine = new TransformationEngine( new DefaultConfiguration($configurationData) );
Creating a Custom Transformer
A custom transformer is at the most basic level an object that implements the AmpProject\Optimizer\Transformer
interface.
This means it needs to have at the very least the following method:
public function transform(Document $document, ErrorCollection $errors) { // Apply transformations to the provided $document and ... // ... add any encountered errors to the $errors collection. }
To make this transformer then known to the transformation engine, you add it to the AmpProject\Optimizer\Configuration::KEY_TRANSFORMERS
key of the AmpProject\Optimizer\Configuration
object you pass into it.
use AmpProject\Optimizer\Configuration; use AmpProject\Optimizer\DefaultConfiguration; use AmpProject\Optimizer\TransformationEngine; use MyProject\MyCustomTransformer; $configurationData = [ Configuration::KEY_TRANSFORMERS => array_merge( Configuration::DEFAULT_TRANSFORMERS, [ MyCustomTransformer::class ], ), ]; $transformationEngine = new TransformationEngine( new DefaultConfiguration($configurationData) );
Making a Custom Transformer Configurable
Configuration objects for the individual transformers need to be registered with the main AmpProject\Optimizer\Configuration
object using its registerConfigurationClass()
method, which takes a fully qualified class name of the transformer as well as a fully qualified class name of the corresponding configuration object as its two arguments.
The configuration objects for the transformers that ship with this library are already registered by default. But if you add third-party or custom transformers, you'll need to register whatever configuration objects they might need with the main AmpProject\Optimizer\Configuration
object first.
In the following example, we add a new MyProject\MyCustomTransformer
transformer in addition to the default set and configure it with a default value, and then we register its corresponding configuration object to make sure the configuration can be properly validated and passed around.
use AmpProject\Optimizer\Configuration; use AmpProject\Optimizer\DefaultConfiguration; use AmpProject\Optimizer\TransformationEngine; use MyProject\MyCustomTransformer; use MyProject\MyCustomTransformerConfiguration; $configurationData = [ Configuration::KEY_TRANSFORMERS => array_merge( Configuration::DEFAULT_TRANSFORMERS, [ MyCustomTransformer::class ], ), MyCustomTransformer::class => [ MyCustomTransformerConfiguration::SOME_CONFIG_KEY => 'some value', ], ]; $configuration = new DefaultConfiguration($configurationData); $configuration->registerConfigurationClass( MyCustomTransformer::class, MyCustomTransformerConfiguration::class ); $transformationEngine = new TransformationEngine($configuration);
For the wiring to work correctly, the MyProject\MyCustomTransformer
class needs to accept within its constructor an object implementing the AmpProject\Optimizer\TransformerConfiguration
interface. The transformation engine will then inject the appropriate implementation at runtime when the transformer is being instantiated.
The MyProject\MyCustomTransformerConfiguration
class should then implement that same AmpProject\Optimizer\TransformerConfiguration
interface. For convenience, it can do so easily by extending the abstract AmpProject\Optimizer\Configuration\BaseTransformerConfiguration
base class.
The configuration object will then be automatically injected into the transformer's constructor as needed.
Here's an example configuration class for our custom MyProject\MyCustomTransformer
transformer:
namespace MyProject; use AmpProject\Optimizer\Configuration\BaseTransformerConfiguration; final class MyCustomTransformerConfiguration extends BaseTransformerConfiguration { const SOME_CONFIG_KEY = 'some_config_key'; protected function getAllowedKeys() { return [ self::SOME_CONFIG_KEY => 'default value', ]; } protected function validate($key, $value) { switch ($key) { case self::SOME_CONFIG_KEY: // Validate configuration value here. } return $value; } }
Here's how the transformer itself can accept and make use of the configuration object:
namespace MyProject; use AmpProject\Dom\Document; use AmpProject\Optimizer\Configurable; use AmpProject\Optimizer\ErrorCollection; use AmpProject\Optimizer\TransformerConfiguration; use AmpProject\Optimizer\Transformer; final class MyCustomTransformer implements Transformer { private $configuration; public function __construct(TransformerConfiguration $configuration) { $this->configuration = $configuration; } public function transform(Document $document, ErrorCollection $errors) { // Bogus transformer logic that adds the configuration value as a body attribute. $document->body->setAttribute( 'data-my-custom-transformer-body-attribute, $this->configuration->get( MyCustomTransformerConfiguration::SOME_CONFIG_KEY ) ); } }
Transformers Requesting External Data
In case your transformer needs to make remote requests to fetch external data (like the AmpProject\Optimizer\Transformer\AmpRuntimeCss
does for fetching the latest version of the CSS to inline), you need to accept an AmpProject\RemoteGetRequest
object as an argument in your constructor. The transformation engine will then inject the appropriate implementation at runtime when the transformer is being instantiated.
This layer of abstraction allows code outside of the transformation engine to control the specific conditions and limits that govern these remote request, like for example throttling them or integrating them with the caching subsystem of the framework in use.
namespace MyProject; use AmpProject\Dom\Document; use AmpProject\RemoteGetRequest; use AmpProject\Optimizer\ErrorCollection; use AmpProject\Optimizer\Transformer; use Throwable; final class MyCustomTransformer implements Transformer { const END_POINT = 'https://example.com/some_endpoint/'; private $remoteRequest; public function __construct(RemoteGetRequest $remoteRequest) { $this->remoteRequest = $remoteRequest; } public function transform(Document $document, ErrorCollection $errors) { try { $response = $this->remoteRequest->get(self::END_POINT); } catch (Throwable $exception) { // Add error handling here. } $statusCode = $response->getStatusCode(); if (200 < $statusCode || $statusCode >= 300) { // Add error handling here. } $content = $response->getBody(); // Make use of the $content you've just retrieved from an external source. } }
Adapting the Handling of Remote Requests
The implementation to use for fulfilling requests made via the AmpProject\RemoteGetRequest
interface can be injected into the AmpProject\Optimizer\TransformationEngine
via its second, optional argument:
use AmpProject\Optimizer\DefaultConfiguration; use AmpProject\Optimizer\TransformationEngine; $transformationEngine = new TransformationEngine( new DefaultConfiguration(), // A custom implementation that lets you control how remote requests are handled. new MyCustomRemoteGetRequestImplementation() );
If this optional second argument is not provided when instancing the transformation engine, the default AmpProject\RemoteRequest\CurlRemoteGetRequest
implementation is used.
There are other implementations already provided that can be useful:
The following code shows an example of how to use a remote request via cURL while falling back to files stored on the disk when an external request fails (probably due to network issues).
use AmpProject\Optimizer\DefaultConfiguration; use AmpProject\Optimizer\TransformationEngine; use AmpProject\RemoteRequest\CurlRemoteGetRequest; use AmpProject\RemoteRequest\FallbackRemoteGetRequest; use AmpProject\RemoteRequest\FilesystemRemoteGetRequest; const FALLBACK_MAPPING = [ 'https://example.com/some_endpoint/' => __DIR__ . '/../fallback_files/some_endpoint.json', ]; $remoteRequest = new FallbackRemoteGetRequest( new CurlRemoteGetRequest(true, 5, 0), // 5 second timeout with no retries, and ... new FilesystemRemoteGetRequest(self::FALLBACK_MAPPING) // ... fall back to shipped files. ); $transformationEngine = new TransformationEngine(new DefaultConfiguration(), $remoteRequest);
To build your own transport, you'll need to implement the AmpProject\RemoteGetRequest
interface. For a more involved example of a custom transport or for integrating with your stack of choice, see the two implementations provided by the Amp for WordPress
WordPress plugin: