diff --git a/README.md b/README.md index 68d1f57..1810d90 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,39 @@
-

AMP Client

+

AMP Client PHP

+

:mega: PHP Client for Advertising Management Platform

+

+Checks +Coverage Status +Total Downloads +Latest Version +PHP Version +

+ ## Installation ```sh $ composer require 68publishers/amp-client ``` + +## Versions compatibility matrix + +| PHP client version | PHP version | AMP version | API version | +|:----------------------:|-------------|:----------------:|:-----------:| +| `^1.0` | `>=7.4` | `>=2.12` | `1` | + + +## Integration without a framework + +The client is not dependent on any framework and can therefore be used independently. +For standalone use, continue to the [Integration without a framework](docs/integration-without-framework.md) section. + +## Integration with Nette Framework + +The client is well integrated into the Nette framework. +For documentation, continue to the [Integration with Nette framework](docs/integration-with-nette-framework.md) section. + +## License + +@todo diff --git a/docs/integration-with-nette-framework.md b/docs/integration-with-nette-framework.md new file mode 100644 index 0000000..6837eda --- /dev/null +++ b/docs/integration-with-nette-framework.md @@ -0,0 +1,175 @@ +# Integration with Nette framework + +For more information on how the client works, we also recommend reading the [Integration without a framework](./integration-without-framework.md) section. + +## Client integration + +The minimum configuration is as follows: + +```neon +extensions: + amp_client: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientExtension + +amp_client: + url: + channel: +``` + +The only mandatory values in the configuration are the AMP application URL and the channel (project) name. +Here are all the configuration options: + +```neon +extensions: + amp_client: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientExtension + +amp_client: + url: + channel: + # Http method, allowed values are GET (default) and POST + method: GET + # Locale for requests (null by default): + locale: en + # AMP API version: + version: 1 + # Default resources for all requests: + default_resources: + category: + - 1 + # Value for http header X-Amp-Origin. + origin: https://www.example.com + + cache: + # Cache store, by default null (cache is disabled): + storage: @Nette\Caching\Storage + # Expiration must be set for caching: + expiration: '1 hour' + # Overrides Cache-Control header in responses from the AMP: + cache_control_header_override: 'max-age=60' + + http: + # Custom Guzzle options: + guzzle_config: [] + + renderer: + # "phtml" or "latte". The bridge is automatically resolved. If you are working on standard Nette application the bridge will be always "latte" + bridge: latte + # Here can be overriden the default templates for each position type: + templates: + single: %appDir%/templates/amp/single.latte + random: %appDir%/templates/amp/random.latte + multiple: %appDir%/templates/amp/multiple.latte + not_found: %appDir%/templates/amp/not_found.latte +``` + +Two important services are now available in the DI Container - `AmpClientInterface` and `RendererInterface`. +You can autowire them into, for example, Presenter or any service: + +```php +use Nette\Application\UI\Presenter; +use SixtyEightPublishers\AmpClient\AmpClientInterface; +use SixtyEightPublishers\AmpClient\Renderer\RendererInterface; + +final class MyPresenter extends Presenter { + public function __construct( + private readonly AmpClientInterface $client, + private readonly RendererInterface $renderer, + ) { + parent::__construct(); + } + + public function actionDefault(): void { + $request = new BannersRequest([ + new Position('homepage.top'), + new Position('homepage.promo', [ + new BannerResource('role', 'guest'), + ]), + ]); + + $response = $this->client->fetchBanners($request); + + bdump($this->renderer->render($response->getPosition('homepage.top'))); + bdump($this->renderer->render($response->getPosition('homepage.promo'))); + } +} +``` + +## Latte macros integration + +Banners can be rendered directly from the Latte template without having to manually call the client. We need to register another extension for this: + +```neon +extensions: + amp_client.latte: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientLatteExtension(%debugMode%) +``` + +Now we have the macro `{banner}` available in the application, and we can use it in templates: + +```latte +{banner homepage.top} +{banner homepage.promo, ['role' => 'guest']} +``` + +Banners are now requested via API and rendered to the template automatically. + +Each `{banner}` macro makes a separate request to the AMP API, so in our example above, two requests are sent. +This can be solved by the following configuration: + +```neon +amp_client.latte: + rendering_mode: queued_in_presenter_context # the default value is "direct" +``` + +Now when rendering a page via `nette/application`, information about all banners to be rendered is collected and a request to the AMP API is sent only once the whole template is rendered. +The banners are then inserted back into the rendered page. This behavior also works automatically with AJAX snippets. + +### Configuring client before the first fetch + +Occasionally, we may want to configure the client before making a request to the AMP API from the template. +For example, we left the `locale` blank in the main `neon` configuration and want to set it up at runtime. +To do this, we can use a custom service implementing the `ConfigureClientEventHandlerInterface` interface. + +```php +use SixtyEightPublishers\AmpClient\Bridge\Latte\Event\ConfigureClientEvent; +use SixtyEightPublishers\AmpClient\Bridge\Latte\Event\ConfigureClientEventHandlerInterface; + +final class SetupLocaleEventHandler implements ConfigureClientEventHandlerInterface +{ + public function __construct( + private readonly MyLocalizationService $localizationService, + ) {} + + public function __invoke(ConfigureClientEvent $event): ConfigureClientEvent + { + $client = $event->getClient(); + $config = $client->getConfig(); + + return $event->withClient( + $client->withConfig( + $config->withLocale($this->localizationService->getCurrentLocale()), + ), + ); + } +} +``` + +And register it: + +```neon +services: + - SetupLocaleEventHandler + # or + - + autowired: self + type: SetupLocaleEventHandler +``` + +Our handler will be called before the first AMP API call from the Latte. + +### Renaming the macro + +Macro `{banner}` can be renamed. The following configuration will rename it to `{ampBanner}`. + +```neon +amp_client.latte: + banner_macro_name: ampBanner +``` diff --git a/docs/integration-without-framework.md b/docs/integration-without-framework.md new file mode 100644 index 0000000..de317d0 --- /dev/null +++ b/docs/integration-without-framework.md @@ -0,0 +1,271 @@ +# Integration without a framework + +## Client initialization + +The client is simply instanced as follows: + +```php +use SixtyEightPublishers\AmpClient\AmpClient; +use SixtyEightPublishers\AmpClient\ClientConfig; + +$config = ClientConfig::create('', ''); +$client = AmpClient::create($config); +``` + +The only mandatory values in the configuration are the AMP application URL and the channel (project) name. +Other optional options are as follows: + +```php +use SixtyEightPublishers\AmpClient\AmpClient; +use SixtyEightPublishers\AmpClient\ClientConfig; +use SixtyEightPublishers\AmpClient\Request\ValueObject\BannerResource; + +$config = ClientConfig::create('', ''); + +# Configure http method, allowed values are GET (default) and POST. +$config = $config->withMethod('POST'); + +# Configure locale for requests (null by default). +$config = $config->withLocale('en'); + +# Configure AMP API version. +$config = $config->withVersion(1); + +# Configure default resources for all requests. +$config = $config->withDefaultResources([ + new BannerResource('category', ['1']), +]); + +# Configure value for http header X-Amp-Origin. +$config = $config->withOrigin('https://www.example.com'); + +# Configure http cache. More about the cache in the documentation below. +$config = $config->withCacheExpiration('1 hour'); +$config->withCacheControlHeaderOverride('max-age=60'); + +$client = AmpClient::create($config); +``` + +> :exclamation: Please note that `ClientConfig` is immutable, just like the other client classes. + +### Cache + +By default, the client uses [NoCacheStorage](../src/Http/Cache/NoCacheStorage.php), so requests are not cached. +This can be changed by setting the cache and its expiration: + +```php +use SixtyEightPublishers\AmpClient\AmpClient; +use SixtyEightPublishers\AmpClient\ClientConfig; +use SixtyEightPublishers\AmpClient\Http\Cache\InMemoryCacheStorage; + +$config = ClientConfig::create('', '') + ->withCacheExpiration('1 hour'); + +$client = AmpClient::create($config) + ->withCacheStorage(new InMemoryCacheStorage()); +``` + +The cache expiration can be set using the DateTime modifier (for example `2 hours`, `1 day` etc.) or an integer that specifies the number of seconds for which the cache should be stored. +Currently, the following storages are implemented: + +- [InMemoryCacheStorage](../src/Http/Cache/InMemoryCacheStorage.php) +- [NetteCacheStorage](../src/Bridge/Nette/NetteCacheStorage.php) + +By default, the cache is controlled by the `Cache-Control` and `ETag` headers that AMP sends in the response. +However, the `Cache-Control` header can be overridden in the configuration: + +```php +$config = $config->withCacheControlHeaderOverride('no-cache'); +``` + +This setting will cache the responses, but a response is revalidated before each use. +The directives that are processed are `no-store`, `no-cache`, `max-age` and `s-maxage`. More information about the `Cache-Control` header [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). + +### Custom Guzzle options + +The client sends requests using Guzzle. If you would like Guzzle to give the client default options, you must instantiate the AMP client with the HTTP client factory. + +```php +use SixtyEightPublishers\AmpClient\AmpClient; +use SixtyEightPublishers\AmpClient\ClientConfig; +use SixtyEightPublishers\AmpClient\Http\HttpClientFactory; +use SixtyEightPublishers\AmpClient\Response\Hydrator\ResponseHydrator; +use SixtyEightPublishers\AmpClient\Response\Hydrator\BannersResponseHydratorHandler; + +$guzzleConfig = [ + # ... guzzle options ... +]; + +$config = ClientConfig::create('', ''); +$client = AmpClient::create( + config: $config, + httpClientFactory: new HttpClientFactory( + responseHydrator: new ResponseHydrator([ + new BannersResponseHydratorHandler(), + ]), + guzzleClientConfig: $guzzleConfig, + ), +); +``` + +## Fetching banners + +```php +use SixtyEightPublishers\AmpClient\AmpClientInterface; +use SixtyEightPublishers\AmpClient\Request\BannersRequest; +use SixtyEightPublishers\AmpClient\Request\ValueObject\Position; +use SixtyEightPublishers\AmpClient\Request\ValueObject\BannerResource; + +/** @var AmpClientInterface $client */ + +$request = new BannersRequest([ + new Position('homepage.top'), + new Position('homepage.promo', [ + new BannerResource('role', 'guest'), + ]), +]); + +$response = $client->fetchBanners($request); # SixtyEightPublishers\AmpClient\Response\BannersResponse + +$homepageTop = $response->getPosition('homepage.top'); +$homepagePromo = $response->getPosition('homepage.promo'); +``` + +## Rendering banners + +Banners can be rendered simply by using the `Renderer` class: + +```php +use SixtyEightPublishers\AmpClient\AmpClientInterface; +use SixtyEightPublishers\AmpClient\Renderer\Renderer; +use SixtyEightPublishers\AmpClient\Response\BannersResponse; + +/** @var BannersResponse $response */ + +$renderer = Renderer::create(); + +echo $renderer->render($response->getPosition('homepage.top')); +``` + +The default templates are written as `.phtml` templates and can be found [here](../src/Renderer/Phtml/Templates). Templates can be also overwritten: + +```php +use SixtyEightPublishers\AmpClient\Renderer\Renderer; +use SixtyEightPublishers\AmpClient\Renderer\Phtml\PhtmlRendererBridge; +use SixtyEightPublishers\AmpClient\Renderer\Templates; + +$bridge = new PhtmlRendererBridge(); +$bridge = $bridge->overrideTemplates(new Templates([ + Templates::TemplateSingle => '/my_custom_template_for_single_position.phtml', +])); + +$renderer = Renderer::create($bridge); +``` + +The following template types can be overwritten: + +```php +use SixtyEightPublishers\AmpClient\Renderer\Templates; + +new Templates([ + Templates::TemplateSingle => '/single.phtml', # for positions with the display type "single" + Templates::TemplateMultiple => '/multiple.phtml', # for positions with the display type "multiple" + Templates::TemplateRandom => '/random.phtml', # for positions with the display type "random" + Templates::TemplateNotFound => '/notFound.phtml', # for positions that were not found +]) +``` + +### Rendering banners using Latte + +Banners can also be rendered using the [Latte](https://github.com/nette/latte) templating system. +Versions `^2.11` and `^3.0` are supported. + +```php +use SixtyEightPublishers\AmpClient\AmpClientInterface; +use SixtyEightPublishers\AmpClient\Renderer\Renderer; +use SixtyEightPublishers\AmpClient\Renderer\Latte\LatteRendererBridge; +use SixtyEightPublishers\AmpClient\Response\BannersResponse; +use SixtyEightPublishers\AmpClient\Renderer\Latte\ClosureLatteFactory; +use Latte\Engine; + +/** @var BannersResponse $response */ + +$renderer = Renderer::create( + LatteRendererBridge::fromEngine(new Engine()), +); + +# or lazily via + +$renderer = Renderer::create( + new LatteRendererBridge( + new ClosureLatteFactory(function (): Engine { + return new Engine(); + }), + ), +); + +echo $renderer->render($response->getPosition('homepage.top')); +``` + +The default `.latte` templates are located [here](../src/Renderer/Latte/Templates) and can be overridden in the same way as the default `.phtml` templates. + +## Latte templating system integration + +In addition to being able to render banners manually using Latte templates, the client offers the ability to render them directly using a custom Latte macro. +The macro is registered as follows: + +```php +use SixtyEightPublishers\AmpClient\AmpClientInterface; +use SixtyEightPublishers\AmpClient\Renderer\RendererInterface; +use SixtyEightPublishers\AmpClient\Bridge\Latte\AmpClientLatteExtension; +use SixtyEightPublishers\AmpClient\Bridge\Latte\RendererProvider; +use Latte\Engine; + +/** @var AmpClientInterface $client */ +/** @var RendererInterface $renderer */ + +$engine = new Engine(); +$provider = (new RendererProvider($client,$renderer)) + ->setDebugMode(true); # exceptions from Client and Renderer are suppressed in non-debug mode + +AmpClientLatteExtension::register($engine, $provider); + +$engine->render(__DIR__ . '/template.latte'); +``` + +```latte +{* ./template.latte *} + +{banner homepage.top} +{banner homepage.promo, ['role' => 'guest']} +``` + +Banners are now requested via API and rendered to the template automatically. + +Each `{banner}` macro makes a separate request to the AMP API, so in our example above, two requests are sent. +This can be solved, however you need to render the Latte to a text string, not a buffer. + +```php +use SixtyEightPublishers\AmpClient\AmpClientInterface; +use SixtyEightPublishers\AmpClient\Renderer\RendererInterface; +use SixtyEightPublishers\AmpClient\Bridge\Latte\AmpClientLatteExtension; +use SixtyEightPublishers\AmpClient\Bridge\Latte\RendererProvider; +use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\QueuedRenderingMode; +use Latte\Engine; + +/** @var AmpClientInterface $client */ +/** @var RendererInterface $renderer */ + +$engine = new Engine(); +$provider = (new RendererProvider($client,$renderer)) + ->setDebugMode(true) # exceptions from Client and Renderer are suppressed in non-debug mode + ->setRenderingMode(new QueuedRenderingMode()); + +AmpClientLatteExtension::register($engine, $provider); + +$output = $engine->renderToString(__DIR__ . '/template.latte'); + +echo $provider->renderQueuedPositions($output); +``` + +Now the client requests both banners in the template with one request.