From b62fec29ed438e3704d3eb2abbf9ed08b54fbc54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Glawaty?= Date: Thu, 30 Nov 2023 04:28:23 +0100 Subject: [PATCH] More rendering modes and docs - added rendering mode `client_side` - added method `RendererBridgeInterface::renderClientSide()` and implementations with templates for `phtml` and `latte` bridges - added method `RendererInterface::renderClientSide()` - removed prefix `Template*` from constants in class `Templates` - added possibility to define alternative rendering modes in `RendererProvider` - added option `alternative_rendering_modes` in `AmpClientLatteExtension` - added option `renderer.templates.client_side` in `AmpClientExtension` - added possibility to switch rendering mode in macro `{banner}` by specifying the argument `mode` - added and fixed tests - updated docs --- README.md | 10 +- docs/images/logo.png | Bin 0 -> 76314 bytes docs/integration-with-nette-framework.md | 41 ++++- docs/integration-without-framework.md | 106 ++++++++++-- src/Bridge/Latte/RendererProvider.php | 131 ++++++++++++--- .../RenderingMode/ClientSideRenderingMode.php | 32 ++++ .../RenderingMode/DirectRenderingMode.php | 12 ++ .../QueuedRenderingInPresenterContextMode.php | 12 ++ .../RenderingMode/QueuedRenderingMode.php | 12 ++ .../RenderingMode/RenderingModeInterface.php | 4 + src/Bridge/Nette/DI/AmpClientExtension.php | 10 +- .../Nette/DI/AmpClientLatteExtension.php | 53 ++++-- .../Nette/DI/Config/AmpClientLatteConfig.php | 5 +- src/Renderer/Latte/LatteRendererBridge.php | 44 +++-- .../Latte/Templates/ClientSideTemplate.php | 32 ++++ src/Renderer/Latte/Templates/clientSide.latte | 5 + src/Renderer/Phtml/PhtmlRendererBridge.php | 40 +++-- src/Renderer/Phtml/Templates/clientSide.phtml | 15 ++ .../Phtml/Templates/contents.fragment.phtml | 4 +- src/Renderer/Phtml/Templates/multiple.phtml | 2 +- src/Renderer/Renderer.php | 28 +++- src/Renderer/RendererBridgeInterface.php | 16 +- src/Renderer/RendererInterface.php | 12 +- src/Renderer/Templates.php | 13 +- .../Latte/AmpClientLatteExtensionTest.php | 64 +++++++ tests/Bridge/Latte/RendererProviderTest.php | 156 ++++++++++++++++++ .../ClientSideRenderingModeTest.php | 29 ++++ .../RenderingMode/DirectRenderingModeTest.php | 29 ++++ ...uedRenderingInPresenterContextModeTest.php | 34 ++++ .../RenderingMode/QueuedRenderingModeTest.php | 29 ++++ .../Nette/DI/AmpClientExtensionTest.php | 9 +- .../Nette/DI/AmpClientLatteExtensionTest.php | 101 +++++++----- .../config.withRendererTemplates.neon | 1 + .../config.withAlternativeRenderingModes.neon | 13 ++ ...ithClientSideRenderingModeAsClassname.neon | 10 ++ ...ithClientSideRenderingModeAsStatement.neon | 10 ++ ...g.withClientSideRenderingModeAsString.neon | 10 ++ .../Latte/LatteRendererBridgeTest.php | 31 +++- .../Phtml/PhtmlRendererBridgeTest.php | 31 +++- tests/Renderer/RendererTest.php | 81 ++++++++- tests/Renderer/TemplatesTest.php | 26 +-- .../renderer/client-side/data-provider.php | 45 +++++ .../renderer/client-side/positionOnly.html | 1 + .../client-side/templates/client-side1.phtml | 10 ++ .../renderer/client-side/withAttributes.html | 1 + .../renderer/client-side/withResources.html | 1 + .../withResourcesAndAttributes.html | 1 + 47 files changed, 1180 insertions(+), 182 deletions(-) create mode 100644 docs/images/logo.png create mode 100644 src/Bridge/Latte/RenderingMode/ClientSideRenderingMode.php create mode 100644 src/Renderer/Latte/Templates/ClientSideTemplate.php create mode 100644 src/Renderer/Latte/Templates/clientSide.latte create mode 100644 src/Renderer/Phtml/Templates/clientSide.phtml create mode 100644 tests/Bridge/Latte/RenderingMode/ClientSideRenderingModeTest.php create mode 100644 tests/Bridge/Latte/RenderingMode/DirectRenderingModeTest.php create mode 100644 tests/Bridge/Latte/RenderingMode/QueuedRenderingInPresenterContextModeTest.php create mode 100644 tests/Bridge/Latte/RenderingMode/QueuedRenderingModeTest.php create mode 100644 tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withAlternativeRenderingModes.neon create mode 100644 tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsClassname.neon create mode 100644 tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsStatement.neon create mode 100644 tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsString.neon create mode 100644 tests/resources/renderer/client-side/data-provider.php create mode 100644 tests/resources/renderer/client-side/positionOnly.html create mode 100644 tests/resources/renderer/client-side/templates/client-side1.phtml create mode 100644 tests/resources/renderer/client-side/withAttributes.html create mode 100644 tests/resources/renderer/client-side/withResources.html create mode 100644 tests/resources/renderer/client-side/withResourcesAndAttributes.html diff --git a/README.md b/README.md index a0747bf..7839355 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@
-

AMP Client PHP

+AMP Client PHP Logo +

AMP Client PHP

:mega: PHP Client for Advertising Management Platform

@@ -19,10 +20,9 @@ $ composer require 68publishers/amp-client ## Versions compatibility matrix -| PHP client version | PHP version | AMP version | API version | -|:----------------------:|-------------|:----------------:|:-----------:| -| `^1.0` | `>=7.4` | `>=2.12` | `1` | - +| PHP client version | PHP version | AMP version | API version | +|:------------------:|-------------|:----------------:|:-----------:| +| `>=1.0` | `>=7.4` | `>=2.12` | `1` | ## Integration without a framework diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..38b29eea10ffa824caa9c117ffe81a44951acf8f GIT binary patch literal 76314 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 s9B!z6JlCoGq` +AMP Client PHP Logo +

AMP Client PHP

+

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](#client-integration) * [Latte macros integration](#latte-macros-integration) + * [Using multiple rendering modes](#using-multiple-rendering-modes) * [Configuring client before the first fetch](#configuring-client-before-the-first-fetch) * [Renaming the macro](#renaming-the-macro) @@ -64,6 +69,7 @@ amp_client: random: %appDir%/templates/amp/random.latte multiple: %appDir%/templates/amp/multiple.latte not_found: %appDir%/templates/amp/not_found.latte + client_side: %appDir%/templates/amp/client_side.latte ``` Two important services are now available in the DI Container - `AmpClientInterface` and `RendererInterface`. @@ -100,23 +106,24 @@ final class MyPresenter extends Presenter { ## 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: +Banners can be rendered directly from the Latte template without having to manually call the client. Another extension must be registered 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: +Now the macro `{banner}` is available in the application and can be used in templates: ```latte {banner homepage.top} {banner homepage.promo, resources: ['role' => 'guest']} +{banner homepage.bottom, attributes: ['class' => 'my-awesome-class']} ``` 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. +Each `{banner}` macro makes a separate request to the AMP API, so in our example above, three requests are sent. This can be solved by the following configuration: ```neon @@ -127,6 +134,32 @@ amp_client.latte: 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. +The following rendering modes are available: + +- **direct** ([DirectRenderingMode](../src/Bridge/Latte/RenderingMode/DirectRenderingMode.php)) - The default mode, API is requested separately for each banner. +- **client_side** ([ClientSideRenderingMode](../src/Bridge/Latte/RenderingMode/ClientSideRenderingMode.php)) - Renders only a wrapper element and leaves loading banners on the JavaScript client. Banners are loaded by calling the `attachBanners()` function. +- **queued_in_presenter_context** ([QueuedRenderingInPresenterContextMode](../src/Bridge/Latte/RenderingMode/QueuedRenderingInPresenterContextMode.php)) - Renders only HTML comments as placeholders and stores requested positions in a queue. It will request, render and place all banners to them positions at once before the presenter returns a response. +- **queued** ([QueuedRenderingMode](../src/Bridge/Latte/RenderingMode/QueuedRenderingMode.php)) - Same behavior as `queued_in_presenter_context`, but it doesn't take into account whether the website template is currently being rendered through the Nette application. It is more suited for an integration without a framework. + +### Using multiple rendering modes + +Besides the default rendering mode, which is set by the option `rendering_mode`, it is possible to configure alternative modes that can be used in templates. + +```neon +amp_client.latte: + rendering_mode: queued_in_presenter_context # the default value is "direct" + alternative_rendering_modes: + - client_side +``` + +```latte +{* The first banner will be rendered with the default mode *} +{banner homepage.top} + +{* The second banner will be rendered client side *} +{banner homepage.promo, mode: 'client_side'} +``` + ### 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. diff --git a/docs/integration-without-framework.md b/docs/integration-without-framework.md index 6432e67..26d7915 100644 --- a/docs/integration-without-framework.md +++ b/docs/integration-without-framework.md @@ -1,11 +1,20 @@ -# Integration without a framework +
+AMP Client PHP Logo +

AMP Client PHP

+

Integration without a framework

+
* [Client initialization](#client-initialization) * [Cache](#cache) * [Custom Guzzle options](#custom-guzzle-options) * [Fetching banners](#fetching-banners) * [Rendering banners](#rendering-banners) + * [Rendering banners on the client side](#rendering-banners-on-the-client-side) + * [Templates overwriting](#templates-overwriting) + * [Rendering banners using Latte](#rendering-banners-using-latte) * [Latte templating system integration](#latte-templating-system-integration) + * [Using multiple rendering modes](#using-multiple-rendering-modes) + * [Renaming the macro](#renaming-the-macro) ## Client initialization @@ -143,7 +152,6 @@ $homepagePromo = $response->getPosition('homepage.promo'); 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; @@ -154,6 +162,30 @@ $renderer = Renderer::create(); echo $renderer->render($response->getPosition('homepage.top')); ``` +The second argument can be used to pass an array of attributes to be contained in the banner's HTML wrapper element. + +```php +echo $renderer->render($response->getPosition('homepage.top', ['class' => 'my-awesome-class'])); +``` + +### Rendering banners on the client side + +Banner rendering can be left to the [JavaScript client](https://github.com/68publishers/amp-client-js) using the `Renderer::renderClientSide()` method. + +```php +use SixtyEightPublishers\AmpClient\Renderer\Renderer; +use SixtyEightPublishers\AmpClient\Request\ValueObject\Position; + +$renderer = Renderer::create(); + +echo $renderer->renderClientSide(new Position('homepage.top')); +echo $renderer->renderClientSide(new Position('homepage.promo'), ['class' => 'my-awesome-class']); +``` + +Banners rendered in this way will be loaded by the JavaScript client when its `attachBanners()` function is called. + +### Templates overwriting + The default templates are written as `.phtml` templates and can be found [here](../src/Renderer/Phtml/Templates). Templates can be also overwritten: ```php @@ -163,7 +195,7 @@ use SixtyEightPublishers\AmpClient\Renderer\Templates; $bridge = new PhtmlRendererBridge(); $bridge = $bridge->overrideTemplates(new Templates([ - Templates::TemplateSingle => '/my_custom_template_for_single_position.phtml', + Templates::Single => '/my_custom_template_for_single_position.phtml', ])); $renderer = Renderer::create($bridge); @@ -175,10 +207,11 @@ The following template types can be overwritten: 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 + Templates::Single => '/single.phtml', # for positions with the display type "single" + Templates::Multiple => '/multiple.phtml', # for positions with the display type "multiple" + Templates::Random => '/random.phtml', # for positions with the display type "random" + Templates::NotFound => '/notFound.phtml', # for positions that were not found + Templates::ClientSide => '/clientSide.phtml', ]) ``` @@ -245,12 +278,13 @@ $engine->render(__DIR__ . '/template.latte'); {banner homepage.top} {banner homepage.promo, resources: ['role' => 'guest']} +{banner homepage.bottom, attributes: ['class' => 'my-awesome-class']} ``` 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. +By default, each `{banner}` macro makes a separate request to the AMP API, so in our example above, three requests are sent. +This can be solved, however you need to render the Latte to a string, not a buffer. ```php use SixtyEightPublishers\AmpClient\AmpClientInterface; @@ -265,7 +299,7 @@ use Latte\Engine; $engine = new Engine(); $provider = (new RendererProvider($client,$renderer)) - ->setDebugMode(true) # exceptions from Client and Renderer are suppressed in non-debug mode + ->setDebugMode(true) ->setRenderingMode(new QueuedRenderingMode()); AmpClientLatteExtension::register($engine, $provider); @@ -276,3 +310,55 @@ echo $provider->renderQueuedPositions($output); ``` Now the client requests both banners in the template with one request. + +The following rendering modes are available: + +- **direct** ([DirectRenderingMode](../src/Bridge/Latte/RenderingMode/DirectRenderingMode.php)) - The default mode, API is requested separately for each banner. +- **client_side** ([ClientSideRenderingMode](../src/Bridge/Latte/RenderingMode/ClientSideRenderingMode.php)) - Renders only a wrapper element and leaves loading banners on the JavaScript client. Banners are loaded by calling the `attachBanners()` function. +- **queued** ([QueuedRenderingMode](../src/Bridge/Latte/RenderingMode/QueuedRenderingMode.php)) - Renders only HTML comments as placeholders and stores requested positions in a queue. It will request and render all banners at once when the `RendererProvider::renderQueuedPositions()` method is called. +- **queued_in_presenter_context** ([QueuedRenderingInPresenterContextMode](../src/Bridge/Latte/RenderingMode/QueuedRenderingInPresenterContextMode.php)) - Same behavior as `queued` but in the context of a Presenter only. Usable with Nette applications only. + +### Using multiple rendering modes + +Besides the default rendering mode, which is set by the method `RendererProvider::setRenderingMode()`, it is possible to configure alternative modes that can be used in templates. + +```php +use SixtyEightPublishers\AmpClient\Bridge\Latte\RendererProvider; +use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\DirectRenderingMode; +use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\ClientSideRenderingMode; + +/** @var AmpClientInterface $client */ +/** @var RendererInterface $renderer */ + +$provider = new RendererProvider($client, $renderer); + +$provider->setRenderingMode(new DirectRenderingMode()); # No need to actually set it up, this mode is the default. +$provider->setAlternativeRenderingModes([ + new ClientSideRenderingMode(), +]); +``` + +```latte +{* The first banner will be rendered with the default mode (directly) *} +{banner homepage.top} + +{* The second banner will be rendered client side *} +{banner homepage.promo, mode: 'client_side'} +``` + +### Renaming the macro + +Macro `{banner}` can be renamed. This can be done by specifying the third argument of the method `AmpClientLatteExtension::register()`. + +```php +use SixtyEightPublishers\AmpClient\Bridge\Latte\AmpClientLatteExtension; +use SixtyEightPublishers\AmpClient\Bridge\Latte\RendererProvider; +use Latte\Engine; + +/** @var RendererProvider $provider */ +/** @var Engine $engine */ + +AmpClientLatteExtension::register($engine, $provider, 'ampBanner'); +``` + +The macro will now be named `{ampBanner}`. diff --git a/src/Bridge/Latte/RendererProvider.php b/src/Bridge/Latte/RendererProvider.php index e2789d7..901b638 100644 --- a/src/Bridge/Latte/RendererProvider.php +++ b/src/Bridge/Latte/RendererProvider.php @@ -4,6 +4,7 @@ namespace SixtyEightPublishers\AmpClient\Bridge\Latte; +use InvalidArgumentException; use Psr\Log\LoggerInterface; use SixtyEightPublishers\AmpClient\AmpClientInterface; use SixtyEightPublishers\AmpClient\Bridge\Latte\Event\ConfigureClientEvent; @@ -23,14 +24,19 @@ use function array_values; use function assert; use function count; +use function gettype; use function htmlspecialchars; use function is_array; +use function is_scalar; +use function is_string; use function sprintf; use function str_replace; final class RendererProvider { private const OptionResources = 'resources'; + private const OptionAttributes = 'attributes'; + private const OptionMode = 'mode'; private AmpClientInterface $client; @@ -40,9 +46,16 @@ final class RendererProvider private RenderingModeInterface $renderingMode; + /** @var array */ + private array $alternativeRenderingModes = []; + private bool $debugMode = false; - /** @var array}> */ + /** @var array, + * }> + */ private array $queue = []; /** @var array> */ @@ -70,13 +83,24 @@ public function __construct( */ public function __invoke(object $globals, string $positionCode, array $options = []): string { + $renderingMode = $this->resolveRenderingMode($options); $position = $this->createPosition($positionCode, $options); - if ($this->renderingMode->shouldBePositionQueued($position, $globals)) { + if ($renderingMode->shouldBePositionRenderedClientSide($position)) { + return $this->renderClientSidePosition($position, $options); + } + + if ($renderingMode->shouldBePositionQueued($position, $globals)) { return $this->addToQueue($position, $options); - } else { - return $this->render($position, $options); } + + $response = $this->fetchResponse(new BannersRequest([$position])); + + if (null === $response || null === $response->getPosition($positionCode)) { + return ''; + } + + return $this->renderPosition($response->getPosition($positionCode), $options); } public function setDebugMode(bool $debugMode): self @@ -93,32 +117,31 @@ public function setRenderingMode(RenderingModeInterface $renderingMode): self return $this; } - public function addConfigureClientEventHandler(ConfigureClientEventHandlerInterface $handler): self + /** + * @param array $renderingModes + */ + public function setAlternativeRenderingModes(array $renderingModes): self { - $this->eventHandlers[ConfigureClientEventHandlerInterface::class][] = $handler; + $this->alternativeRenderingModes = []; + + foreach ($renderingModes as $renderingMode) { + assert($renderingMode instanceof RenderingModeInterface); + + $this->alternativeRenderingModes[$renderingMode->getName()] = $renderingMode; + } return $this; } - /** - * @param array $options - * - * @throws AmpExceptionInterface - */ - private function render(RequestPosition $position, array $options): string + public function addConfigureClientEventHandler(ConfigureClientEventHandlerInterface $handler): self { - $positionCode = $position->getCode(); - $response = $this->fetchResponse(new BannersRequest([$position])); - - if (null === $response || null === $response->getPosition($positionCode)) { - return ''; - } + $this->eventHandlers[ConfigureClientEventHandlerInterface::class][] = $handler; - return $this->renderPosition($response->getPosition($positionCode), $options); + return $this; } /** - * @param array $options * + * @param array $options */ private function addToQueue(RequestPosition $position, array $options): string { @@ -130,7 +153,17 @@ private function addToQueue(RequestPosition $position, array $options): string public function supportsQueues(): bool { - return $this->renderingMode->supportsQueues(); + if ($this->renderingMode->supportsQueues()) { + return true; + } + + foreach ($this->alternativeRenderingModes as $alternativeRenderingMode) { + if ($alternativeRenderingMode->supportsQueues()) { + return true; + } + } + + return false; } public function isAnythingQueued(): bool @@ -245,7 +278,7 @@ private function fetchResponse(BannersRequest $request): ?BannersResponse private function renderPosition(ResponsePosition $position, array $options): string { try { - $elementAttributes = (array) ($options['attributes'] ?? []); + $elementAttributes = (array) ($options[self::OptionAttributes] ?? []); return $this->renderer->render($position, $elementAttributes); } catch (RendererException $e) { @@ -263,6 +296,30 @@ private function renderPosition(ResponsePosition $position, array $options): str } } + /** + * @param array $options + */ + private function renderClientSidePosition(RequestPosition $position, array $options): string + { + try { + $elementAttributes = (array) ($options[self::OptionAttributes] ?? []); + + return $this->renderer->renderClientSide($position, $elementAttributes); + } catch (RendererException $e) { + if ($this->debugMode) { + throw $e; + } + + if (null !== $this->logger) { + $this->logger->error($e->getMessage(), [ + 'exception' => $e, + ]); + } + + return ''; + } + } + /** * @param array $options */ @@ -278,6 +335,36 @@ private function createPosition(string $positionCode, array $options): RequestPo return new RequestPosition($positionCode, $bannerResources); } + /** + * @param array $options + */ + private function resolveRenderingMode(array $options): RenderingModeInterface + { + if (!isset($options[self::OptionMode])) { + return $this->renderingMode; + } + + $mode = $options[self::OptionMode]; + + if ($mode instanceof RenderingModeInterface) { + $mode = $mode->getName(); + } + + if ($this->renderingMode->getName() === $mode) { + return $this->renderingMode; + } + + if (!is_string($mode) || !isset($this->alternativeRenderingModes[$mode])) { + throw new InvalidArgumentException(sprintf( + 'Invalid value for option "%s". The value %s is not registered between alternative rendering modes.', + self::OptionMode, + is_scalar($mode) ? "\"$mode\"" : gettype($mode), + )); + } + + return $this->alternativeRenderingModes[$mode]; + } + private function formatHtmlComment(string $positionCode): string { return sprintf( diff --git a/src/Bridge/Latte/RenderingMode/ClientSideRenderingMode.php b/src/Bridge/Latte/RenderingMode/ClientSideRenderingMode.php new file mode 100644 index 0000000..51eadfe --- /dev/null +++ b/src/Bridge/Latte/RenderingMode/ClientSideRenderingMode.php @@ -0,0 +1,32 @@ +uiPresenter) && $globals->uiPresenter instanceof Presenter; } + + public function shouldBePositionRenderedClientSide(Position $position): bool + { + return false; + } } diff --git a/src/Bridge/Latte/RenderingMode/QueuedRenderingMode.php b/src/Bridge/Latte/RenderingMode/QueuedRenderingMode.php index 32ff3b0..dde6eff 100644 --- a/src/Bridge/Latte/RenderingMode/QueuedRenderingMode.php +++ b/src/Bridge/Latte/RenderingMode/QueuedRenderingMode.php @@ -8,6 +8,13 @@ final class QueuedRenderingMode implements RenderingModeInterface { + public const Name = 'queued'; + + public function getName(): string + { + return self::Name; + } + public function supportsQueues(): bool { return true; @@ -17,4 +24,9 @@ public function shouldBePositionQueued(Position $position, object $globals): boo { return true; } + + public function shouldBePositionRenderedClientSide(Position $position): bool + { + return false; + } } diff --git a/src/Bridge/Latte/RenderingMode/RenderingModeInterface.php b/src/Bridge/Latte/RenderingMode/RenderingModeInterface.php index 6231bf7..d7f8f5e 100644 --- a/src/Bridge/Latte/RenderingMode/RenderingModeInterface.php +++ b/src/Bridge/Latte/RenderingMode/RenderingModeInterface.php @@ -8,7 +8,11 @@ interface RenderingModeInterface { + public function getName(): string; + public function supportsQueues(): bool; public function shouldBePositionQueued(Position $position, object $globals): bool; + + public function shouldBePositionRenderedClientSide(Position $position): bool; } diff --git a/src/Bridge/Nette/DI/AmpClientExtension.php b/src/Bridge/Nette/DI/AmpClientExtension.php index e2f03b5..024451e 100644 --- a/src/Bridge/Nette/DI/AmpClientExtension.php +++ b/src/Bridge/Nette/DI/AmpClientExtension.php @@ -99,6 +99,7 @@ public function getConfigSchema(): Schema 'random' => Expect::string(), 'multiple' => Expect::string(), 'not_found' => Expect::string(), + 'client_side' => Expect::string(), ])->castTo('array'), ])->castTo(RendererConfig::class), ])->castTo(AmpClientConfig::class); @@ -273,10 +274,11 @@ private function resolveRendererBridgeCreator(RendererConfig $config): Statement } $templatesOverride = array_filter([ - Templates::TemplateSingle => $config->templates['single'] ?? null, - Templates::TemplateRandom => $config->templates['random'] ?? null, - Templates::TemplateMultiple => $config->templates['multiple'] ?? null, - Templates::TemplateNotFound => $config->templates['not_found'] ?? null, + Templates::Single => $config->templates['single'] ?? null, + Templates::Random => $config->templates['random'] ?? null, + Templates::Multiple => $config->templates['multiple'] ?? null, + Templates::NotFound => $config->templates['not_found'] ?? null, + Templates::ClientSide => $config->templates['client_side'] ?? null, ]); if (0 < count($templatesOverride)) { diff --git a/src/Bridge/Nette/DI/AmpClientLatteExtension.php b/src/Bridge/Nette/DI/AmpClientLatteExtension.php index e12a794..77115dd 100644 --- a/src/Bridge/Nette/DI/AmpClientLatteExtension.php +++ b/src/Bridge/Nette/DI/AmpClientLatteExtension.php @@ -20,11 +20,13 @@ use SixtyEightPublishers\AmpClient\Bridge\Latte\AmpClientLatteExtension as AmpClientLatteExtensionRegister; use SixtyEightPublishers\AmpClient\Bridge\Latte\Event\ConfigureClientEventHandlerInterface; use SixtyEightPublishers\AmpClient\Bridge\Latte\RendererProvider; +use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\ClientSideRenderingMode; use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\DirectRenderingMode; use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\QueuedRenderingInPresenterContextMode; use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\QueuedRenderingMode; use SixtyEightPublishers\AmpClient\Bridge\Nette\Application\AttachPresenterHandlersOnApplicationHandler; use SixtyEightPublishers\AmpClient\Bridge\Nette\DI\Config\AmpClientLatteConfig; +use function array_map; use function assert; use function count; use function sprintf; @@ -32,9 +34,10 @@ final class AmpClientLatteExtension extends CompilerExtension { private const RenderingModes = [ - 'direct' => DirectRenderingMode::class, - 'queued' => QueuedRenderingMode::class, - 'queued_in_presenter_context' => QueuedRenderingInPresenterContextMode::class, + DirectRenderingMode::Name => DirectRenderingMode::class, + QueuedRenderingMode::Name => QueuedRenderingMode::class, + QueuedRenderingInPresenterContextMode::Name => QueuedRenderingInPresenterContextMode::class, + ClientSideRenderingMode::Name => ClientSideRenderingMode::class, ]; private bool $debugMode; @@ -50,6 +53,9 @@ public function getConfigSchema(): Schema 'banner_macro_name' => Expect::string('banner'), 'rendering_mode' => Expect::anyOf(Expect::string(), Expect::type(Statement::class)) ->default('direct'), + 'alternative_rendering_modes' => Expect::listOf( + Expect::anyOf(Expect::string(), Expect::type(Statement::class)), + ), ])->castTo(AmpClientLatteConfig::class); } @@ -75,25 +81,26 @@ public function loadConfiguration(): void $config = $this->getConfig(); assert($config instanceof AmpClientLatteConfig); - $renderingMode = $config->rendering_mode; - - if (is_string($renderingMode) && isset(self::RenderingModes[$renderingMode])) { - $renderingMode = self::RenderingModes[$renderingMode]; - } - - if (!($renderingMode instanceof Statement)) { - $renderingMode = new Statement($renderingMode); - } - - $builder->addDefinition($this->prefix('rendererProvider')) + $rendererProviderDefinition = $builder->addDefinition($this->prefix('rendererProvider')) ->setAutowired(false) ->setFactory(RendererProvider::class) ->addSetup('setRenderingMode', [ - 'renderingMode' => $renderingMode, + 'renderingMode' => $this->createRenderingModeStatement($config->rendering_mode), ]) ->addSetup('setDebugMode', [ 'debugMode' => $this->debugMode, ]); + + $alternativeRenderingModes = array_map( + fn ($renderingMode): Statement => $this->createRenderingModeStatement($renderingMode), + $config->alternative_rendering_modes, + ); + + if (0 < count($alternativeRenderingModes)) { + $rendererProviderDefinition->addSetup('setAlternativeRenderingModes', [ + 'renderingModes' => $alternativeRenderingModes, + ]); + } } public function beforeCompile(): void @@ -142,4 +149,20 @@ private function extensionExists(string $classname): bool { return 0 < count($this->compiler->getExtensions($classname)); } + + /** + * @param string|Statement $renderingMode + */ + private function createRenderingModeStatement($renderingMode): Statement + { + if (is_string($renderingMode) && isset(self::RenderingModes[$renderingMode])) { + $renderingMode = self::RenderingModes[$renderingMode]; + } + + if (!($renderingMode instanceof Statement)) { + $renderingMode = new Statement($renderingMode); + } + + return $renderingMode; + } } diff --git a/src/Bridge/Nette/DI/Config/AmpClientLatteConfig.php b/src/Bridge/Nette/DI/Config/AmpClientLatteConfig.php index c8ba8c7..4e6189d 100644 --- a/src/Bridge/Nette/DI/Config/AmpClientLatteConfig.php +++ b/src/Bridge/Nette/DI/Config/AmpClientLatteConfig.php @@ -10,6 +10,9 @@ final class AmpClientLatteConfig { public string $banner_macro_name; - /** @var string|Statement|null */ + /** @var string|Statement */ public $rendering_mode; + + /** @var array */ + public array $alternative_rendering_modes = []; } diff --git a/src/Renderer/Latte/LatteRendererBridge.php b/src/Renderer/Latte/LatteRendererBridge.php index 19d8a67..e0a5250 100644 --- a/src/Renderer/Latte/LatteRendererBridge.php +++ b/src/Renderer/Latte/LatteRendererBridge.php @@ -5,14 +5,17 @@ namespace SixtyEightPublishers\AmpClient\Renderer\Latte; use Latte\Engine; +use SixtyEightPublishers\AmpClient\Renderer\Latte\Templates\ClientSideTemplate; use SixtyEightPublishers\AmpClient\Renderer\Latte\Templates\MultipleTemplate; use SixtyEightPublishers\AmpClient\Renderer\Latte\Templates\NotFoundTemplate; use SixtyEightPublishers\AmpClient\Renderer\Latte\Templates\RandomTemplate; use SixtyEightPublishers\AmpClient\Renderer\Latte\Templates\SingleTemplate; use SixtyEightPublishers\AmpClient\Renderer\RendererBridgeInterface; use SixtyEightPublishers\AmpClient\Renderer\Templates; +use SixtyEightPublishers\AmpClient\Request\ValueObject\Position as RequestPosition; use SixtyEightPublishers\AmpClient\Response\ValueObject\Banner; -use SixtyEightPublishers\AmpClient\Response\ValueObject\Position; +use SixtyEightPublishers\AmpClient\Response\ValueObject\Position as ResponsePosition; +use function implode; final class LatteRendererBridge implements RendererBridgeInterface { @@ -26,10 +29,11 @@ public function __construct(LatteFactoryInterface $latteFactory) { $this->latteFactory = $latteFactory; $this->templates = new Templates([ - Templates::TemplateSingle => __DIR__ . '/Templates/single.latte', - Templates::TemplateRandom => __DIR__ . '/Templates/random.latte', - Templates::TemplateMultiple => __DIR__ . '/Templates/multiple.latte', - Templates::TemplateNotFound => __DIR__ . '/Templates/notFound.latte', + Templates::Single => __DIR__ . '/Templates/single.latte', + Templates::Random => __DIR__ . '/Templates/random.latte', + Templates::Multiple => __DIR__ . '/Templates/multiple.latte', + Templates::NotFound => __DIR__ . '/Templates/notFound.latte', + Templates::ClientSide => __DIR__ . '/Templates/clientSide.latte', ]); } @@ -48,38 +52,52 @@ public function overrideTemplates(Templates $templates): self return $renderer; } - public function renderNotFound(Position $position, array $elementAttributes = []): string + public function renderNotFound(ResponsePosition $position, array $elementAttributes = []): string { return $this->getLatte()->renderToString( - $this->templates->getTemplateFile(Templates::TemplateNotFound), + $this->templates->getTemplateFile(Templates::NotFound), new NotFoundTemplate($position, $elementAttributes), ); } - public function renderSingle(Position $position, ?Banner $banner, array $elementAttributes = []): string + public function renderSingle(ResponsePosition $position, ?Banner $banner, array $elementAttributes = []): string { return $this->getLatte()->renderToString( - $this->templates->getTemplateFile(Templates::TemplateSingle), + $this->templates->getTemplateFile(Templates::Single), new SingleTemplate($position, $banner, $elementAttributes), ); } - public function renderRandom(Position $position, ?Banner $banner, array $elementAttributes = []): string + public function renderRandom(ResponsePosition $position, ?Banner $banner, array $elementAttributes = []): string { return $this->getLatte()->renderToString( - $this->templates->getTemplateFile(Templates::TemplateRandom), + $this->templates->getTemplateFile(Templates::Random), new RandomTemplate($position, $banner, $elementAttributes), ); } - public function renderMultiple(Position $position, array $banners, array $elementAttributes = []): string + public function renderMultiple(ResponsePosition $position, array $banners, array $elementAttributes = []): string { return $this->getLatte()->renderToString( - $this->templates->getTemplateFile(Templates::TemplateMultiple), + $this->templates->getTemplateFile(Templates::Multiple), new MultipleTemplate($position, $banners, $elementAttributes), ); } + public function renderClientSide(RequestPosition $position, array $elementAttributes = []): string + { + $resourceAttributes = []; + + foreach ($position->getResources() as $resource) { + $resourceAttributes['data-amp-resource-' . $resource->getCode()] = implode(',', $resource->getValues()); + } + + return $this->getLatte()->renderToString( + $this->templates->getTemplateFile(Templates::ClientSide), + new ClientSideTemplate($position, $resourceAttributes, $elementAttributes), + ); + } + private function getLatte(): Engine { if (null === $this->latte) { diff --git a/src/Renderer/Latte/Templates/ClientSideTemplate.php b/src/Renderer/Latte/Templates/ClientSideTemplate.php new file mode 100644 index 0000000..2c273dc --- /dev/null +++ b/src/Renderer/Latte/Templates/ClientSideTemplate.php @@ -0,0 +1,32 @@ + */ + public array $resourceAttributes; + + /** @var array */ + public array $elementAttributes; + + /** + * @param array $resourceAttributes + * @param array $elementAttributes + */ + public function __construct( + Position $position, + array $resourceAttributes, + array $elementAttributes + ) { + $this->position = $position; + $this->resourceAttributes = $resourceAttributes; + $this->elementAttributes = $elementAttributes; + } +} diff --git a/src/Renderer/Latte/Templates/clientSide.latte b/src/Renderer/Latte/Templates/clientSide.latte new file mode 100644 index 0000000..feeef15 --- /dev/null +++ b/src/Renderer/Latte/Templates/clientSide.latte @@ -0,0 +1,5 @@ +{templateType SixtyEightPublishers\AmpClient\Renderer\Latte\Templates\ClientSideTemplate} + +
+
diff --git a/src/Renderer/Phtml/PhtmlRendererBridge.php b/src/Renderer/Phtml/PhtmlRendererBridge.php index 1f4467b..9031b48 100644 --- a/src/Renderer/Phtml/PhtmlRendererBridge.php +++ b/src/Renderer/Phtml/PhtmlRendererBridge.php @@ -7,8 +7,9 @@ use SixtyEightPublishers\AmpClient\Renderer\OutputBuffer; use SixtyEightPublishers\AmpClient\Renderer\RendererBridgeInterface; use SixtyEightPublishers\AmpClient\Renderer\Templates; +use SixtyEightPublishers\AmpClient\Request\ValueObject\Position as RequestPosition; use SixtyEightPublishers\AmpClient\Response\ValueObject\Banner; -use SixtyEightPublishers\AmpClient\Response\ValueObject\Position; +use SixtyEightPublishers\AmpClient\Response\ValueObject\Position as ResponsePosition; use Throwable; final class PhtmlRendererBridge implements RendererBridgeInterface @@ -18,10 +19,11 @@ final class PhtmlRendererBridge implements RendererBridgeInterface public function __construct() { $this->templates = new Templates([ - Templates::TemplateSingle => __DIR__ . '/Templates/single.phtml', - Templates::TemplateRandom => __DIR__ . '/Templates/random.phtml', - Templates::TemplateMultiple => __DIR__ . '/Templates/multiple.phtml', - Templates::TemplateNotFound => __DIR__ . '/Templates/notFound.phtml', + Templates::Single => __DIR__ . '/Templates/single.phtml', + Templates::Random => __DIR__ . '/Templates/random.phtml', + Templates::Multiple => __DIR__ . '/Templates/multiple.phtml', + Templates::NotFound => __DIR__ . '/Templates/notFound.phtml', + Templates::ClientSide => __DIR__ . '/Templates/clientSide.phtml', ]); } @@ -36,9 +38,9 @@ public function overrideTemplates(Templates $templates): self /** * @throws Throwable */ - public function renderNotFound(Position $position, array $elementAttributes = []): string + public function renderNotFound(ResponsePosition $position, array $elementAttributes = []): string { - $filename = $this->templates->getTemplateFile(Templates::TemplateNotFound); + $filename = $this->templates->getTemplateFile(Templates::NotFound); return OutputBuffer::capture(function () use ($filename, $position, $elementAttributes) { require $filename; @@ -48,9 +50,9 @@ public function renderNotFound(Position $position, array $elementAttributes = [] /** * @throws Throwable */ - public function renderSingle(Position $position, ?Banner $banner, array $elementAttributes = []): string + public function renderSingle(ResponsePosition $position, ?Banner $banner, array $elementAttributes = []): string { - $filename = $this->templates->getTemplateFile(Templates::TemplateSingle); + $filename = $this->templates->getTemplateFile(Templates::Single); return OutputBuffer::capture(function () use ($filename, $position, $banner, $elementAttributes) { require $filename; @@ -60,9 +62,9 @@ public function renderSingle(Position $position, ?Banner $banner, array $element /** * @throws Throwable */ - public function renderRandom(Position $position, ?Banner $banner, array $elementAttributes = []): string + public function renderRandom(ResponsePosition $position, ?Banner $banner, array $elementAttributes = []): string { - $filename = $this->templates->getTemplateFile(Templates::TemplateRandom); + $filename = $this->templates->getTemplateFile(Templates::Random); return OutputBuffer::capture(function () use ($filename, $position, $banner, $elementAttributes) { require $filename; @@ -72,12 +74,24 @@ public function renderRandom(Position $position, ?Banner $banner, array $element /** * @throws Throwable */ - public function renderMultiple(Position $position, array $banners, array $elementAttributes = []): string + public function renderMultiple(ResponsePosition $position, array $banners, array $elementAttributes = []): string { - $filename = $this->templates->getTemplateFile(Templates::TemplateMultiple); + $filename = $this->templates->getTemplateFile(Templates::Multiple); return OutputBuffer::capture(function () use ($filename, $position, $banners, $elementAttributes) { require $filename; }); } + + /** + * @throws Throwable + */ + public function renderClientSide(RequestPosition $position, array $elementAttributes = []): string + { + $filename = $this->templates->getTemplateFile(Templates::ClientSide); + + return OutputBuffer::capture(function () use ($filename, $position, $elementAttributes) { + require $filename; + }); + } } diff --git a/src/Renderer/Phtml/Templates/clientSide.phtml b/src/Renderer/Phtml/Templates/clientSide.phtml new file mode 100644 index 0000000..84c2ea8 --- /dev/null +++ b/src/Renderer/Phtml/Templates/clientSide.phtml @@ -0,0 +1,15 @@ + $elementAttributes */ +?> +
getResources() as $resource) : ?>getCode() . '="' . implode(',', $resource->getValues()) . '"' ?> + > +
diff --git a/src/Renderer/Phtml/Templates/contents.fragment.phtml b/src/Renderer/Phtml/Templates/contents.fragment.phtml index 58ce465..d4e4232 100644 --- a/src/Renderer/Phtml/Templates/contents.fragment.phtml +++ b/src/Renderer/Phtml/Templates/contents.fragment.phtml @@ -13,14 +13,14 @@ use SixtyEightPublishers\AmpClient\Renderer\BreakpointStyle\BreakpointStyle; /** @var Position $position */ /** @var ?Banner $banner */ ?> -getContents() as $content): ?> +getContents() as $content) : ?> getTarget()) : ?>target="getTarget()) ?>"> - getSources() as $source): ?> + getSources() as $source) : ?> diff --git a/src/Renderer/Phtml/Templates/multiple.phtml b/src/Renderer/Phtml/Templates/multiple.phtml index 890864c..583d717 100644 --- a/src/Renderer/Phtml/Templates/multiple.phtml +++ b/src/Renderer/Phtml/Templates/multiple.phtml @@ -20,7 +20,7 @@ use SixtyEightPublishers\AmpClient\Renderer\AmpBannerExternalAttribute;
- +
diff --git a/src/Renderer/Renderer.php b/src/Renderer/Renderer.php index 8bfeef1..54ce0d3 100644 --- a/src/Renderer/Renderer.php +++ b/src/Renderer/Renderer.php @@ -6,7 +6,8 @@ use SixtyEightPublishers\AmpClient\Exception\RendererException; use SixtyEightPublishers\AmpClient\Renderer\Phtml\PhtmlRendererBridge; -use SixtyEightPublishers\AmpClient\Response\ValueObject\Position; +use SixtyEightPublishers\AmpClient\Request\ValueObject\Position as RequestPosition; +use SixtyEightPublishers\AmpClient\Response\ValueObject\Position as ResponsePosition; use Throwable; use function get_class; @@ -32,25 +33,25 @@ public static function create(?RendererBridgeInterface $rendererBridge = null): ); } - public function render(Position $position, array $elementAttributes = []): string + public function render(ResponsePosition $position, array $elementAttributes = []): string { try { switch ($position->getDisplayType()) { case null: return $this->rendererBridge->renderNotFound($position, $elementAttributes); - case Position::DisplayTypeMultiple: + case ResponsePosition::DisplayTypeMultiple: return $this->rendererBridge->renderMultiple( $position, $this->bannersResolver->resolveMultiple($position), $elementAttributes, ); - case Position::DisplayTypeRandom: + case ResponsePosition::DisplayTypeRandom: return $this->rendererBridge->renderRandom( $position, $this->bannersResolver->resolveRandom($position), $elementAttributes, ); - case Position::DisplayTypeSingle: + case ResponsePosition::DisplayTypeSingle: default: return $this->rendererBridge->renderSingle( $position, @@ -70,4 +71,21 @@ public function render(Position $position, array $elementAttributes = []): strin ); } } + + public function renderClientSide(RequestPosition $position, array $elementAttributes = []): string + { + try { + return $this->rendererBridge->renderClientSide($position, $elementAttributes); + } catch (Throwable $e) { + if ($e instanceof RendererException) { + throw $e; + } + + throw RendererException::rendererBridgeThrownError( + get_class($this->rendererBridge), + $position->getCode(), + $e, + ); + } + } } diff --git a/src/Renderer/RendererBridgeInterface.php b/src/Renderer/RendererBridgeInterface.php index 5f12254..5c6288f 100644 --- a/src/Renderer/RendererBridgeInterface.php +++ b/src/Renderer/RendererBridgeInterface.php @@ -4,8 +4,9 @@ namespace SixtyEightPublishers\AmpClient\Renderer; +use SixtyEightPublishers\AmpClient\Request\ValueObject\Position as RequestPosition; use SixtyEightPublishers\AmpClient\Response\ValueObject\Banner; -use SixtyEightPublishers\AmpClient\Response\ValueObject\Position; +use SixtyEightPublishers\AmpClient\Response\ValueObject\Position as ResponsePosition; interface RendererBridgeInterface { @@ -14,21 +15,26 @@ public function overrideTemplates(Templates $templates): self; /** * @param array $elementAttributes */ - public function renderNotFound(Position $position, array $elementAttributes = []): string; + public function renderNotFound(ResponsePosition $position, array $elementAttributes = []): string; /** * @param array $elementAttributes */ - public function renderSingle(Position $position, ?Banner $banner, array $elementAttributes = []): string; + public function renderSingle(ResponsePosition $position, ?Banner $banner, array $elementAttributes = []): string; /** * @param array $elementAttributes */ - public function renderRandom(Position $position, ?Banner $banner, array $elementAttributes = []): string; + public function renderRandom(ResponsePosition $position, ?Banner $banner, array $elementAttributes = []): string; /** * @param array $banners * @param array $elementAttributes */ - public function renderMultiple(Position $position, array $banners, array $elementAttributes = []): string; + public function renderMultiple(ResponsePosition $position, array $banners, array $elementAttributes = []): string; + + /** + * @param array $elementAttributes + */ + public function renderClientSide(RequestPosition $position, array $elementAttributes = []): string; } diff --git a/src/Renderer/RendererInterface.php b/src/Renderer/RendererInterface.php index b928ef4..d977788 100644 --- a/src/Renderer/RendererInterface.php +++ b/src/Renderer/RendererInterface.php @@ -5,7 +5,8 @@ namespace SixtyEightPublishers\AmpClient\Renderer; use SixtyEightPublishers\AmpClient\Exception\RendererException; -use SixtyEightPublishers\AmpClient\Response\ValueObject\Position; +use SixtyEightPublishers\AmpClient\Request\ValueObject\Position as RequestPosition; +use SixtyEightPublishers\AmpClient\Response\ValueObject\Position as ResponsePosition; interface RendererInterface { @@ -14,5 +15,12 @@ interface RendererInterface * * @throws RendererException */ - public function render(Position $position, array $elementAttributes = []): string; + public function render(ResponsePosition $position, array $elementAttributes = []): string; + + /** + * @param array $elementAttributes + * + * @throws RendererException + */ + public function renderClientSide(RequestPosition $position, array $elementAttributes = []): string; } diff --git a/src/Renderer/Templates.php b/src/Renderer/Templates.php index b40f489..7f350f1 100644 --- a/src/Renderer/Templates.php +++ b/src/Renderer/Templates.php @@ -9,17 +9,19 @@ final class Templates { - public const TemplateSingle = 'single'; - public const TemplateRandom = 'random'; - public const TemplateMultiple = 'multiple'; - public const TemplateNotFound = 'notFound'; + public const Single = 'single'; + public const Random = 'random'; + public const Multiple = 'multiple'; + public const NotFound = 'notFound'; + public const ClientSide = 'clientSide'; /** * @var array{ * single?: string, * random?: string, * multiple?: string, - * 'notFound'?: string, + * notFound?: string, + * clientSide?: string, * } */ private array $filesMap; @@ -30,6 +32,7 @@ final class Templates * random?: string, * multiple?: string, * notFound?: string, + * clientSide?: string, * } $filesMap */ public function __construct(array $filesMap) diff --git a/tests/Bridge/Latte/AmpClientLatteExtensionTest.php b/tests/Bridge/Latte/AmpClientLatteExtensionTest.php index cf487e0..f5f5e24 100644 --- a/tests/Bridge/Latte/AmpClientLatteExtensionTest.php +++ b/tests/Bridge/Latte/AmpClientLatteExtensionTest.php @@ -116,6 +116,70 @@ public function latteTemplatesDataProvider(): array ], 2 => null, ], + 'Attributes as array' => [ + 0 => <<<'LATTE' + {banner homepage.top, attributes: ['class' => 'test-class']} + LATTE, + 1 => [ + 'homepage.top', + [ + 'attributes' => ['class' => 'test-class'], + ], + ], + 2 => null, + ], + 'Attributes as variable' => [ + 0 => <<<'LATTE' + {var $attributes = ['class' => 'test-class']} + {banner homepage.top, attributes: $attributes} + LATTE, + 1 => [ + 'homepage.top', + [ + 'attributes' => ['class' => 'test-class'], + ], + ], + 2 => null, + ], + 'Mode as string' => [ + 0 => <<<'LATTE' + {banner homepage.top, mode: 'client_side'} + LATTE, + 1 => [ + 'homepage.top', + [ + 'mode' => 'client_side', + ], + ], + 2 => null, + ], + 'Mode as variable' => [ + 0 => <<<'LATTE' + {var $mode = 'client_side'} + {banner homepage.top, mode: $mode} + LATTE, + 1 => [ + 'homepage.top', + [ + 'mode' => 'client_side', + ], + ], + 2 => null, + ], + 'Full featured' => [ + 0 => <<<'LATTE' + {banner homepage.top, resources: [product => '123', category => ['123', '456']], attributes: ['class' => 'test-class'], mode: 'client_side'} + LATTE, + 1 => [ + 'homepage.top', + [ + 'resources' => ['product' => '123', 'category' => ['123', '456']], + 'attributes' => ['class' => 'test-class'], + 'mode' => 'client_side', + ], + ], + 2 => null, + ], ]; } diff --git a/tests/Bridge/Latte/RendererProviderTest.php b/tests/Bridge/Latte/RendererProviderTest.php index b91855b..dcfb660 100644 --- a/tests/Bridge/Latte/RendererProviderTest.php +++ b/tests/Bridge/Latte/RendererProviderTest.php @@ -4,6 +4,7 @@ namespace SixtyEightPublishers\AmpClient\Tests\Bridge\Latte; +use InvalidArgumentException; use Mockery; use Psr\Log\LoggerInterface; use SixtyEightPublishers\AmpClient\AmpClientInterface; @@ -62,6 +63,41 @@ public function testInvokingDefaultInstanceWithoutResources(): void Assert::same('', $provider(new stdClass(), 'homepage.top')); } + public function testInvokingDefaultInstanceWithSameModeAsDefault(): void + { + $client = Mockery::mock(AmpClientInterface::class); + $renderer = Mockery::mock(RendererInterface::class); + $provider = new RendererProvider($client, $renderer); + + $responsePosition = new ResponsePosition('1234', 'homepage.top', 'Homepage top', 0, ResponsePosition::DisplayTypeSingle, ResponsePosition::BreakpointTypeMin, []); + $response = new BannersResponse([ + 'homepage.top' => $responsePosition, + ]); + + $client + ->shouldReceive('fetchBanners') + ->once() + ->with(Mockery::type(BannersRequest::class)) + ->andReturnUsing(static function (BannersRequest $request) use ($response): BannersResponse { + Assert::equal( + new BannersRequest([ + new RequestPosition('homepage.top'), + ]), + $request, + ); + + return $response; + }); + + $renderer + ->shouldReceive('render') + ->once() + ->with($responsePosition, []) + ->andReturn(''); + + Assert::same('', $provider(new stdClass(), 'homepage.top', ['mode' => 'direct'])); + } + public function testInvokingDefaultInstanceWithAttributes(): void { $client = Mockery::mock(AmpClientInterface::class); @@ -360,6 +396,22 @@ public function testPositionsShouldBeQueuedAndReplacedInStringOutput(): void $provider->setRenderingMode($renderingMode); $renderingMode + ->shouldReceive('shouldBePositionRenderedClientSide') + ->once() + ->with(Mockery::type(RequestPosition::class)) + ->andReturnUsing(static function (RequestPosition $position) use ($requestPosition1): bool { + Assert::equal($requestPosition1, $position); + + return false; + }) + ->shouldReceive('shouldBePositionRenderedClientSide') + ->once() + ->with(Mockery::type(RequestPosition::class)) + ->andReturnUsing(static function (RequestPosition $position) use ($requestPosition2): bool { + Assert::equal($requestPosition2, $position); + + return false; + }) ->shouldReceive('shouldBePositionQueued') ->once() ->with(Mockery::type(RequestPosition::class), $globals) @@ -436,6 +488,22 @@ public function testPositionsShouldBeQueuedAndReplacedInArrayOutput(): void $provider->setRenderingMode($renderingMode); $renderingMode + ->shouldReceive('shouldBePositionRenderedClientSide') + ->once() + ->with(Mockery::type(RequestPosition::class)) + ->andReturnUsing(static function (RequestPosition $position) use ($requestPosition1): bool { + Assert::equal($requestPosition1, $position); + + return false; + }) + ->shouldReceive('shouldBePositionRenderedClientSide') + ->once() + ->with(Mockery::type(RequestPosition::class)) + ->andReturnUsing(static function (RequestPosition $position) use ($requestPosition2): bool { + Assert::equal($requestPosition2, $position); + + return false; + }) ->shouldReceive('shouldBePositionQueued') ->once() ->with(Mockery::type(RequestPosition::class), $globals) @@ -504,6 +572,94 @@ public function testPositionsShouldBeQueuedAndReplacedInArrayOutput(): void ); } + public function testPositionShouldBeRenderedClientSide(): void + { + $client = Mockery::mock(AmpClientInterface::class); + $renderer = Mockery::mock(RendererInterface::class); + $clientSideRenderingMode = Mockery::mock(RenderingModeInterface::class); + $requestPosition = new RequestPosition('homepage.top'); + + $provider = new RendererProvider($client, $renderer); + + $provider->setRenderingMode($clientSideRenderingMode); + + $clientSideRenderingMode + ->shouldReceive('shouldBePositionRenderedClientSide') + ->once() + ->with(Mockery::type(RequestPosition::class)) + ->andReturnUsing(static function (RequestPosition $position) use ($requestPosition): bool { + Assert::equal($requestPosition, $position); + + return true; + }); + + $renderer + ->shouldReceive('renderClientSide') + ->once() + ->with(Mockery::type(RequestPosition::class), []) + ->andReturnUsing(static function (RequestPosition $position) use ($requestPosition): string { + Assert::equal($requestPosition, $position); + + return ''; + }); + + Assert::same('', $provider(new stdClass(), 'homepage.top')); + } + + public function testPositionShouldBeRenderedClientSideWithAlternativeMode(): void + { + $client = Mockery::mock(AmpClientInterface::class); + $renderer = Mockery::mock(RendererInterface::class); + $clientSideRenderingMode = Mockery::mock(RenderingModeInterface::class); + $requestPosition = new RequestPosition('homepage.top'); + + $provider = new RendererProvider($client, $renderer); + + $clientSideRenderingMode + ->shouldReceive('getName') + ->once() + ->withNoArgs() + ->andReturn('client_side'); + + $clientSideRenderingMode + ->shouldReceive('shouldBePositionRenderedClientSide') + ->once() + ->with(Mockery::type(RequestPosition::class)) + ->andReturnUsing(static function (RequestPosition $position) use ($requestPosition): bool { + Assert::equal($requestPosition, $position); + + return true; + }); + + $renderer + ->shouldReceive('renderClientSide') + ->once() + ->with(Mockery::type(RequestPosition::class), []) + ->andReturnUsing(static function (RequestPosition $position) use ($requestPosition): string { + Assert::equal($requestPosition, $position); + + return ''; + }); + + $provider->setAlternativeRenderingModes([$clientSideRenderingMode]); + + Assert::same('', $provider(new stdClass(), 'homepage.top', ['mode' => 'client_side'])); + } + + public function testExceptionShouldBeThrownWhenProviderIsInvokedWithModeThatIsNotRegisteredBetweenAlternativeModes(): void + { + $client = Mockery::mock(AmpClientInterface::class); + $renderer = Mockery::mock(RendererInterface::class); + + $provider = new RendererProvider($client, $renderer); + + Assert::exception( + static fn () => $provider(new stdClass(), 'homepage.top', ['mode' => 'test']), + InvalidArgumentException::class, + 'Invalid value for option "mode". The value "test" is not registered between alternative rendering modes.', + ); + } + protected function tearDown(): void { Mockery::close(); diff --git a/tests/Bridge/Latte/RenderingMode/ClientSideRenderingModeTest.php b/tests/Bridge/Latte/RenderingMode/ClientSideRenderingModeTest.php new file mode 100644 index 0000000..70cdff5 --- /dev/null +++ b/tests/Bridge/Latte/RenderingMode/ClientSideRenderingModeTest.php @@ -0,0 +1,29 @@ +getName()); + Assert::false($mode->supportsQueues()); + Assert::true($mode->shouldBePositionRenderedClientSide($position)); + Assert::false($mode->shouldBePositionQueued($position, (object) [])); + } +} + +(new ClientSideRenderingModeTest())->run(); diff --git a/tests/Bridge/Latte/RenderingMode/DirectRenderingModeTest.php b/tests/Bridge/Latte/RenderingMode/DirectRenderingModeTest.php new file mode 100644 index 0000000..6c4f99f --- /dev/null +++ b/tests/Bridge/Latte/RenderingMode/DirectRenderingModeTest.php @@ -0,0 +1,29 @@ +getName()); + Assert::false($mode->supportsQueues()); + Assert::false($mode->shouldBePositionRenderedClientSide($position)); + Assert::false($mode->shouldBePositionQueued($position, (object) [])); + } +} + +(new DirectRenderingModeTest())->run(); diff --git a/tests/Bridge/Latte/RenderingMode/QueuedRenderingInPresenterContextModeTest.php b/tests/Bridge/Latte/RenderingMode/QueuedRenderingInPresenterContextModeTest.php new file mode 100644 index 0000000..c36cdf6 --- /dev/null +++ b/tests/Bridge/Latte/RenderingMode/QueuedRenderingInPresenterContextModeTest.php @@ -0,0 +1,34 @@ +getName()); + Assert::true($mode->supportsQueues()); + Assert::false($mode->shouldBePositionRenderedClientSide($position)); + + Assert::false($mode->shouldBePositionQueued($position, (object) [])); + Assert::true($mode->shouldBePositionQueued($position, (object) [ + 'uiPresenter' => new class extends Presenter {}, + ])); + } +} + +(new QueuedRenderingInPresenterContextModeTest())->run(); diff --git a/tests/Bridge/Latte/RenderingMode/QueuedRenderingModeTest.php b/tests/Bridge/Latte/RenderingMode/QueuedRenderingModeTest.php new file mode 100644 index 0000000..3831ab2 --- /dev/null +++ b/tests/Bridge/Latte/RenderingMode/QueuedRenderingModeTest.php @@ -0,0 +1,29 @@ +getName()); + Assert::true($mode->supportsQueues()); + Assert::false($mode->shouldBePositionRenderedClientSide($position)); + Assert::true($mode->shouldBePositionQueued($position, (object) [])); + } +} + +(new QueuedRenderingModeTest())->run(); diff --git a/tests/Bridge/Nette/DI/AmpClientExtensionTest.php b/tests/Bridge/Nette/DI/AmpClientExtensionTest.php index 55f6995..8f1a03b 100644 --- a/tests/Bridge/Nette/DI/AmpClientExtensionTest.php +++ b/tests/Bridge/Nette/DI/AmpClientExtensionTest.php @@ -137,10 +137,11 @@ public function testContainerWithRendererTemplates(): void $templates = call_user_func(Closure::bind(static fn (): Templates => $rendererBridge->templates, null, PhtmlRendererBridge::class)); assert($templates instanceof Templates); - Assert::same(__DIR__ . '/../../../resources/renderer/single/templates/single1.phtml', $templates->getTemplateFile(Templates::TemplateSingle)); - Assert::same(__DIR__ . '/../../../resources/renderer/random/templates/random1.phtml', $templates->getTemplateFile(Templates::TemplateRandom)); - Assert::same(__DIR__ . '/../../../resources/renderer/multiple/templates/multiple1.phtml', $templates->getTemplateFile(Templates::TemplateMultiple)); - Assert::same(__DIR__ . '/../../../resources/renderer/not-found/templates/not-found1.phtml', $templates->getTemplateFile(Templates::TemplateNotFound)); + Assert::same(__DIR__ . '/../../../resources/renderer/single/templates/single1.phtml', $templates->getTemplateFile(Templates::Single)); + Assert::same(__DIR__ . '/../../../resources/renderer/random/templates/random1.phtml', $templates->getTemplateFile(Templates::Random)); + Assert::same(__DIR__ . '/../../../resources/renderer/multiple/templates/multiple1.phtml', $templates->getTemplateFile(Templates::Multiple)); + Assert::same(__DIR__ . '/../../../resources/renderer/not-found/templates/not-found1.phtml', $templates->getTemplateFile(Templates::NotFound)); + Assert::same(__DIR__ . '/../../../resources/renderer/client-side/templates/client-side1.phtml', $templates->getTemplateFile(Templates::ClientSide)); } public function testContainerWithMethodOption(): void diff --git a/tests/Bridge/Nette/DI/AmpClientLatteExtensionTest.php b/tests/Bridge/Nette/DI/AmpClientLatteExtensionTest.php index 5bbf2be..123cda2 100644 --- a/tests/Bridge/Nette/DI/AmpClientLatteExtensionTest.php +++ b/tests/Bridge/Nette/DI/AmpClientLatteExtensionTest.php @@ -12,6 +12,7 @@ use RuntimeException; use SixtyEightPublishers\AmpClient\Bridge\Latte\Event\ConfigureClientEventHandlerInterface; use SixtyEightPublishers\AmpClient\Bridge\Latte\RendererProvider; +use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\ClientSideRenderingMode; use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\DirectRenderingMode; use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\QueuedRenderingInPresenterContextMode; use SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\QueuedRenderingMode; @@ -53,9 +54,6 @@ public function testContainerWithMinimalConfiguration(): void $container, false, new DirectRenderingMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); } @@ -67,9 +65,6 @@ public function testContainerWithMinimalConfigurationAndApplicationEnabled(): vo $container, false, new DirectRenderingMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); $this->assertApplicationHandlerAttached($container); @@ -83,9 +78,6 @@ public function testContainerWithDebugMode(): void $container, true, new DirectRenderingMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); } @@ -97,9 +89,6 @@ public function testContainerWithDirectRenderingModeAsString(): void $container, false, new DirectRenderingMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); } @@ -111,9 +100,6 @@ public function testContainerWithDirectRenderingModeAsClassname(): void $container, false, new DirectRenderingMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); } @@ -125,9 +111,6 @@ public function testContainerWithDirectRenderingModeAsStatement(): void $container, false, new DirectRenderingMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); } @@ -139,9 +122,6 @@ public function testContainerWithQueuedRenderingModeAsString(): void $container, false, new QueuedRenderingMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); } @@ -153,9 +133,6 @@ public function testContainerWithQueuedRenderingModeAsClassname(): void $container, false, new QueuedRenderingMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); } @@ -167,9 +144,6 @@ public function testContainerWithQueuedRenderingModeAsStatement(): void $container, false, new QueuedRenderingMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); } @@ -181,9 +155,6 @@ public function testContainerWithQueuedRenderingInPresenterModeModeAsString(): v $container, false, new QueuedRenderingInPresenterContextMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); } @@ -195,9 +166,6 @@ public function testContainerWithQueuedRenderingInPresenterModeModeAsClassname() $container, false, new QueuedRenderingInPresenterContextMode(), - [ - ConfigureClientEventHandlerInterface::class => [], - ], ); } @@ -209,8 +177,54 @@ public function testContainerWithQueuedRenderingInPresenterModeModeAsStatement() $container, false, new QueuedRenderingInPresenterContextMode(), + ); + } + + public function testContainerWithClientSideRenderingInPresenterModeModeAsString(): void + { + $container = ContainerFactory::create(__DIR__ . '/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsString.neon', ['latte']); + + $this->assertLatteExtension( + $container, + false, + new ClientSideRenderingMode(), + ); + } + + public function testContainerWithClientSideRenderingInPresenterModeModeAsClassname(): void + { + $container = ContainerFactory::create(__DIR__ . '/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsClassname.neon', ['latte']); + + $this->assertLatteExtension( + $container, + false, + new ClientSideRenderingMode(), + ); + } + + public function testContainerWithClientSideRenderingInPresenterModeModeAsStatement(): void + { + $container = ContainerFactory::create(__DIR__ . '/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsStatement.neon', ['latte']); + + $this->assertLatteExtension( + $container, + false, + new ClientSideRenderingMode(), + ); + } + + public function testContainerWithAlternativeRenderingModes(): void + { + $container = ContainerFactory::create(__DIR__ . '/Config/AmpClientLatteExtension/config.withAlternativeRenderingModes.neon', ['latte']); + + $this->assertLatteExtension( + $container, + false, + new DirectRenderingMode(), [ - ConfigureClientEventHandlerInterface::class => [], + ClientSideRenderingMode::Name => new ClientSideRenderingMode(), + QueuedRenderingMode::Name => new QueuedRenderingMode(), + QueuedRenderingInPresenterContextMode::Name => new QueuedRenderingInPresenterContextMode(), ], ); } @@ -223,6 +237,7 @@ public function testContainerWithConfigureClientEventHandler(): void $container, false, new DirectRenderingMode(), + [], [ ConfigureClientEventHandlerInterface::class => [ new ConfigureClientEventHandlerFixture(null), @@ -231,8 +246,19 @@ public function testContainerWithConfigureClientEventHandler(): void ); } - private function assertLatteExtension(Container $container, bool $debugMode, RenderingModeInterface $renderingMode, array $eventHandlers): void - { + private function assertLatteExtension( + Container $container, + bool $debugMode, + RenderingModeInterface $renderingMode, + array $alternativeRenderingModes = [], + ?array $eventHandlers = null + ): void { + if (null === $eventHandlers) { + $eventHandlers = [ + ConfigureClientEventHandlerInterface::class => [], + ]; + } + $latteFactory = $container->getByType(class_exists(LatteFactory::class) ? LatteFactory::class : ILatteFactory::class); $latte = $latteFactory->create(); $providers = $latte->getProviders(); @@ -244,9 +270,10 @@ private function assertLatteExtension(Container $container, bool $debugMode, Ren Assert::type(RendererProvider::class, $provider); call_user_func(Closure::bind( - static function () use ($provider, $debugMode, $renderingMode, $eventHandlers): void { + static function () use ($provider, $debugMode, $renderingMode, $alternativeRenderingModes, $eventHandlers): void { Assert::same($debugMode, $provider->debugMode); Assert::equal($renderingMode, $provider->renderingMode); + Assert::equal($alternativeRenderingModes, $provider->alternativeRenderingModes); Assert::equal($eventHandlers, $provider->eventHandlers); }, null, diff --git a/tests/Bridge/Nette/DI/Config/AmpClientExtension/config.withRendererTemplates.neon b/tests/Bridge/Nette/DI/Config/AmpClientExtension/config.withRendererTemplates.neon index c58b89e..c20d0f6 100644 --- a/tests/Bridge/Nette/DI/Config/AmpClientExtension/config.withRendererTemplates.neon +++ b/tests/Bridge/Nette/DI/Config/AmpClientExtension/config.withRendererTemplates.neon @@ -11,3 +11,4 @@ amp_client: random: %resources%/renderer/random/templates/random1.phtml multiple: %resources%/renderer/multiple/templates/multiple1.phtml not_found: %resources%/renderer/not-found/templates/not-found1.phtml + client_side: %resources%/renderer/client-side/templates/client-side1.phtml diff --git a/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withAlternativeRenderingModes.neon b/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withAlternativeRenderingModes.neon new file mode 100644 index 0000000..13d52f5 --- /dev/null +++ b/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withAlternativeRenderingModes.neon @@ -0,0 +1,13 @@ +extensions: + amp_client: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientExtension + amp_client.latte: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientLatteExtension + +amp_client: + url: https://www.example.com + channel: test + +amp_client.latte: + alternative_rendering_modes: + - client_side + - SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\QueuedRenderingMode + - SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\QueuedRenderingInPresenterContextMode() diff --git a/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsClassname.neon b/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsClassname.neon new file mode 100644 index 0000000..478f947 --- /dev/null +++ b/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsClassname.neon @@ -0,0 +1,10 @@ +extensions: + amp_client: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientExtension + amp_client.latte: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientLatteExtension + +amp_client: + url: https://www.example.com + channel: test + +amp_client.latte: + rendering_mode: SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\ClientSideRenderingMode diff --git a/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsStatement.neon b/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsStatement.neon new file mode 100644 index 0000000..bd83df6 --- /dev/null +++ b/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsStatement.neon @@ -0,0 +1,10 @@ +extensions: + amp_client: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientExtension + amp_client.latte: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientLatteExtension + +amp_client: + url: https://www.example.com + channel: test + +amp_client.latte: + rendering_mode: SixtyEightPublishers\AmpClient\Bridge\Latte\RenderingMode\ClientSideRenderingMode() diff --git a/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsString.neon b/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsString.neon new file mode 100644 index 0000000..4d6416c --- /dev/null +++ b/tests/Bridge/Nette/DI/Config/AmpClientLatteExtension/config.withClientSideRenderingModeAsString.neon @@ -0,0 +1,10 @@ +extensions: + amp_client: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientExtension + amp_client.latte: SixtyEightPublishers\AmpClient\Bridge\Nette\DI\AmpClientLatteExtension + +amp_client: + url: https://www.example.com + channel: test + +amp_client.latte: + rendering_mode: client_side diff --git a/tests/Renderer/Latte/LatteRendererBridgeTest.php b/tests/Renderer/Latte/LatteRendererBridgeTest.php index 251524f..8a81229 100644 --- a/tests/Renderer/Latte/LatteRendererBridgeTest.php +++ b/tests/Renderer/Latte/LatteRendererBridgeTest.php @@ -8,8 +8,9 @@ use Latte\Engine; use SixtyEightPublishers\AmpClient\Renderer\Latte\LatteRendererBridge; use SixtyEightPublishers\AmpClient\Renderer\Templates; +use SixtyEightPublishers\AmpClient\Request\ValueObject\Position as RequestPosition; use SixtyEightPublishers\AmpClient\Response\ValueObject\Banner; -use SixtyEightPublishers\AmpClient\Response\ValueObject\Position; +use SixtyEightPublishers\AmpClient\Response\ValueObject\Position as ResponsePosition; use SixtyEightPublishers\AmpClient\Tests\Renderer\AssertHtml; use Tester\Assert; use Tester\TestCase; @@ -23,7 +24,7 @@ public function testTemplatesShouldBeOverridden(): void { $renderer = $this->createRendererBridge(); $modifiedRenderer = $renderer->overrideTemplates(new Templates([ - Templates::TemplateSingle => '/path/to/file', + Templates::Single => '/path/to/file', ])); $originalTemplates = call_user_func(Closure::bind(static fn () => $renderer->templates, null, LatteRendererBridge::class)); @@ -37,7 +38,7 @@ public function testTemplatesShouldBeOverridden(): void * @dataProvider notFoundTemplateDataProvider */ public function testNotFoundTemplateRendering( - Position $position, + ResponsePosition $position, array $elementAttributes, string $expectationFile ): void { @@ -50,7 +51,7 @@ public function testNotFoundTemplateRendering( * @dataProvider singleTemplateDataProvider */ public function testSingleTemplateRendering( - Position $position, + ResponsePosition $position, ?Banner $banner, array $elementAttributes, string $expectationFile @@ -64,7 +65,7 @@ public function testSingleTemplateRendering( * @dataProvider randomTemplateDataProvider */ public function testRandomTemplateRendering( - Position $position, + ResponsePosition $position, ?Banner $banner, array $elementAttributes, string $expectationFile @@ -78,7 +79,7 @@ public function testRandomTemplateRendering( * @dataProvider multipleTemplateDataProvider * */ public function testMultipleTemplateRendering( - Position $position, + ResponsePosition $position, array $banners, array $elementAttributes, string $expectationFile @@ -88,6 +89,19 @@ public function testMultipleTemplateRendering( AssertHtml::assert($expectationFile, $renderer->renderMultiple($position, $banners, $elementAttributes)); } + /** + * @dataProvider clientSideTemplateDataProvider + */ + public function testClientSideTemplateRendering( + RequestPosition $position, + array $elementAttributes, + string $expectationFile + ): void { + $renderer = $this->createRendererBridge(); + + AssertHtml::assert($expectationFile, $renderer->renderClientSide($position, $elementAttributes)); + } + public function notFoundTemplateDataProvider(): array { return require __DIR__ . '/../../resources/renderer/not-found/data-provider.php'; @@ -108,6 +122,11 @@ public function multipleTemplateDataProvider(): array return require __DIR__ . '/../../resources/renderer/multiple/data-provider.php'; } + public function clientSideTemplateDataProvider(): array + { + return require __DIR__ . '/../../resources/renderer/client-side/data-provider.php'; + } + private function createRendererBridge(): LatteRendererBridge { return LatteRendererBridge::fromEngine(new Engine()); diff --git a/tests/Renderer/Phtml/PhtmlRendererBridgeTest.php b/tests/Renderer/Phtml/PhtmlRendererBridgeTest.php index 593dbfc..2c70a31 100644 --- a/tests/Renderer/Phtml/PhtmlRendererBridgeTest.php +++ b/tests/Renderer/Phtml/PhtmlRendererBridgeTest.php @@ -7,8 +7,9 @@ use Closure; use SixtyEightPublishers\AmpClient\Renderer\Phtml\PhtmlRendererBridge; use SixtyEightPublishers\AmpClient\Renderer\Templates; +use SixtyEightPublishers\AmpClient\Request\ValueObject\Position as RequestPosition; use SixtyEightPublishers\AmpClient\Response\ValueObject\Banner; -use SixtyEightPublishers\AmpClient\Response\ValueObject\Position; +use SixtyEightPublishers\AmpClient\Response\ValueObject\Position as ResponsePosition; use SixtyEightPublishers\AmpClient\Tests\Renderer\AssertHtml; use Tester\Assert; use Tester\TestCase; @@ -22,7 +23,7 @@ public function testTemplatesShouldBeOverridden(): void { $renderer = new PhtmlRendererBridge(); $modifiedRenderer = $renderer->overrideTemplates(new Templates([ - Templates::TemplateSingle => '/path/to/file', + Templates::Single => '/path/to/file', ])); $originalTemplates = call_user_func(Closure::bind(static fn () => $renderer->templates, null, PhtmlRendererBridge::class)); @@ -36,7 +37,7 @@ public function testTemplatesShouldBeOverridden(): void * @dataProvider notFoundTemplateDataProvider */ public function testNotFoundTemplateRendering( - Position $position, + ResponsePosition $position, array $elementAttributes, string $expectationFile ): void { @@ -49,7 +50,7 @@ public function testNotFoundTemplateRendering( * @dataProvider singleTemplateDataProvider */ public function testSingleTemplateRendering( - Position $position, + ResponsePosition $position, ?Banner $banner, array $elementAttributes, string $expectationFile @@ -63,7 +64,7 @@ public function testSingleTemplateRendering( * @dataProvider randomTemplateDataProvider */ public function testRandomTemplateRendering( - Position $position, + ResponsePosition $position, ?Banner $banner, array $elementAttributes, string $expectationFile @@ -77,7 +78,7 @@ public function testRandomTemplateRendering( * @dataProvider multipleTemplateDataProvider */ public function testMultipleTemplateRendering( - Position $position, + ResponsePosition $position, array $banners, array $elementAttributes, string $expectationFile @@ -87,6 +88,19 @@ public function testMultipleTemplateRendering( AssertHtml::assert($expectationFile, $renderer->renderMultiple($position, $banners, $elementAttributes)); } + /** + * @dataProvider clientSideTemplateDataProvider + */ + public function testClientSideTemplateRendering( + RequestPosition $position, + array $elementAttributes, + string $expectationFile + ): void { + $renderer = new PhtmlRendererBridge(); + + AssertHtml::assert($expectationFile, $renderer->renderClientSide($position, $elementAttributes)); + } + public function notFoundTemplateDataProvider(): array { return require __DIR__ . '/../../resources/renderer/not-found/data-provider.php'; @@ -106,6 +120,11 @@ public function multipleTemplateDataProvider(): array { return require __DIR__ . '/../../resources/renderer/multiple/data-provider.php'; } + + public function clientSideTemplateDataProvider(): array + { + return require __DIR__ . '/../../resources/renderer/client-side/data-provider.php'; + } } (new PhtmlRendererBridgeTest())->run(); diff --git a/tests/Renderer/RendererTest.php b/tests/Renderer/RendererTest.php index 5ed981c..6bf4e3a 100644 --- a/tests/Renderer/RendererTest.php +++ b/tests/Renderer/RendererTest.php @@ -13,8 +13,10 @@ use SixtyEightPublishers\AmpClient\Renderer\Phtml\PhtmlRendererBridge; use SixtyEightPublishers\AmpClient\Renderer\Renderer; use SixtyEightPublishers\AmpClient\Renderer\RendererBridgeInterface; +use SixtyEightPublishers\AmpClient\Request\ValueObject\BannerResource; +use SixtyEightPublishers\AmpClient\Request\ValueObject\Position as RequestPosition; use SixtyEightPublishers\AmpClient\Response\ValueObject\Banner; -use SixtyEightPublishers\AmpClient\Response\ValueObject\Position; +use SixtyEightPublishers\AmpClient\Response\ValueObject\Position as ResponsePosition; use Tester\Assert; use Tester\TestCase; use function call_user_func; @@ -44,7 +46,7 @@ public function testNotFoundTemplateShouldBeRendered(): void $rendererBridge = Mockery::mock(RendererBridgeInterface::class); $renderer = new Renderer($bannersResolver, $rendererBridge); - $position = new Position(null, 'homepage.top', null, 0, null, Position::BreakpointTypeMin, []); + $position = new ResponsePosition(null, 'homepage.top', null, 0, null, ResponsePosition::BreakpointTypeMin, []); $rendererBridge ->shouldReceive('renderNotFound') @@ -62,7 +64,7 @@ public function testSingleTemplateShouldBeRendered(): void $renderer = new Renderer($bannersResolver, $rendererBridge); $banner = new Banner('1234', 'Main', 0, null, null, null, []); - $position = new Position('1234', 'homepage.top', 'Homepage top', 0, Position::DisplayTypeSingle, Position::BreakpointTypeMin, [$banner]); + $position = new ResponsePosition('1234', 'homepage.top', 'Homepage top', 0, ResponsePosition::DisplayTypeSingle, ResponsePosition::BreakpointTypeMin, [$banner]); $bannersResolver ->shouldReceive('resolveSingle') @@ -86,7 +88,7 @@ public function testRandomTemplateShouldBeRendered(): void $renderer = new Renderer($bannersResolver, $rendererBridge); $banner = new Banner('1234', 'Main', 0, null, null, null, []); - $position = new Position('1234', 'homepage.top', 'Homepage top', 0, Position::DisplayTypeRandom, Position::BreakpointTypeMin, [$banner]); + $position = new ResponsePosition('1234', 'homepage.top', 'Homepage top', 0, ResponsePosition::DisplayTypeRandom, ResponsePosition::BreakpointTypeMin, [$banner]); $bannersResolver ->shouldReceive('resolveRandom') @@ -113,7 +115,7 @@ public function testMultipleTemplateShouldBeRendered(): void new Banner('1234', 'Main', 0, null, null, null, []), new Banner('1235', 'Secondary', 0, null, null, null, []), ]; - $position = new Position('1234', 'homepage.top', 'Homepage top', 0, Position::DisplayTypeMultiple, Position::BreakpointTypeMin, $banners); + $position = new ResponsePosition('1234', 'homepage.top', 'Homepage top', 0, ResponsePosition::DisplayTypeMultiple, ResponsePosition::BreakpointTypeMin, $banners); $bannersResolver ->shouldReceive('resolveMultiple') @@ -130,13 +132,32 @@ public function testMultipleTemplateShouldBeRendered(): void Assert::same('multiple', $renderer->render($position)); } - public function testRendererExceptionShouldBeThrownWhenBridgeThrowsTheException(): void + public function testClientSideTemplateShouldBeRendered(): void { $bannersResolver = Mockery::mock(BannersResolverInterface::class); $rendererBridge = Mockery::mock(RendererBridgeInterface::class); $renderer = new Renderer($bannersResolver, $rendererBridge); - $position = new Position(null, 'homepage.top', null, 0, null, Position::BreakpointTypeMin, []); + $position = new RequestPosition('homepage.top', [ + new BannerResource('role', 'vip'), + ]); + + $rendererBridge + ->shouldReceive('renderClientSide') + ->once() + ->with($position, []) + ->andReturn('client-side'); + + Assert::same('client-side', $renderer->renderClientSide($position)); + } + + public function testRendererExceptionShouldBeThrownOnRenderingWhenBridgeThrowsTheException(): void + { + $bannersResolver = Mockery::mock(BannersResolverInterface::class); + $rendererBridge = Mockery::mock(RendererBridgeInterface::class); + $renderer = new Renderer($bannersResolver, $rendererBridge); + + $position = new ResponsePosition(null, 'homepage.top', null, 0, null, ResponsePosition::BreakpointTypeMin, []); $rendererBridge ->shouldReceive('renderNotFound') @@ -151,13 +172,34 @@ public function testRendererExceptionShouldBeThrownWhenBridgeThrowsTheException( ); } - public function testRendererExceptionShouldBeThrownWhenBridgeThrowsAnyException(): void + public function testRendererExceptionShouldBeThrownOnClientSideRenderingWhenBridgeThrowsTheException(): void + { + $bannersResolver = Mockery::mock(BannersResolverInterface::class); + $rendererBridge = Mockery::mock(RendererBridgeInterface::class); + $renderer = new Renderer($bannersResolver, $rendererBridge); + + $position = new RequestPosition('homepage.top'); + + $rendererBridge + ->shouldReceive('renderClientSide') + ->once() + ->with($position, []) + ->andThrow(new RendererException('Test exception')); + + Assert::exception( + static fn () => $renderer->renderClientSide($position), + RendererException::class, + 'Test exception', + ); + } + + public function testRendererExceptionShouldBeThrownOnRenderingWhenBridgeThrowsAnyException(): void { $bannersResolver = Mockery::mock(BannersResolverInterface::class); $rendererBridge = Mockery::mock(RendererBridgeInterface::class); $renderer = new Renderer($bannersResolver, $rendererBridge); - $position = new Position(null, 'homepage.top', null, 0, null, Position::BreakpointTypeMin, []); + $position = new ResponsePosition(null, 'homepage.top', null, 0, null, ResponsePosition::BreakpointTypeMin, []); $rendererBridge ->shouldReceive('renderNotFound') @@ -172,6 +214,27 @@ public function testRendererExceptionShouldBeThrownWhenBridgeThrowsAnyException( ); } + public function testRendererExceptionShouldBeThrownOnClientSideRenderingWhenBridgeThrowsAnyException(): void + { + $bannersResolver = Mockery::mock(BannersResolverInterface::class); + $rendererBridge = Mockery::mock(RendererBridgeInterface::class); + $renderer = new Renderer($bannersResolver, $rendererBridge); + + $position = new RequestPosition('homepage.top'); + + $rendererBridge + ->shouldReceive('renderClientSide') + ->once() + ->with($position, []) + ->andThrow(new Exception('Test exception')); + + Assert::exception( + static fn () => $renderer->renderClientSide($position), + RendererException::class, + 'Renderer bridge of type %A% thrown an exception while rendering a position homepage.top: Test exception', + ); + } + protected function tearDown(): void { Mockery::close(); diff --git a/tests/Renderer/TemplatesTest.php b/tests/Renderer/TemplatesTest.php index d549e42..ec1c4cb 100644 --- a/tests/Renderer/TemplatesTest.php +++ b/tests/Renderer/TemplatesTest.php @@ -17,11 +17,11 @@ final class TemplatesTest extends TestCase public function testExceptionShouldBeThrownWhenTemplateFileNotFound(): void { $templates = new Templates([ - Templates::TemplateSingle => __DIR__ . '/path/to/missing-file.phtml', + Templates::Single => __DIR__ . '/path/to/missing-file.phtml', ]); Assert::exception( - static fn () => $templates->getTemplateFile(Templates::TemplateSingle), + static fn () => $templates->getTemplateFile(Templates::Single), RendererException::class, 'Template file "%A%/path/to/missing-file.phtml" not found.', ); @@ -30,11 +30,11 @@ public function testExceptionShouldBeThrownWhenTemplateFileNotFound(): void public function testExceptionShouldBeThrownWhenTemplateFileNotDefined(): void { $templates = new Templates([ - Templates::TemplateSingle => __DIR__ . '/path/to/missing-file.phtml', + Templates::Single => __DIR__ . '/path/to/missing-file.phtml', ]); Assert::exception( - static fn () => $templates->getTemplateFile(Templates::TemplateMultiple), + static fn () => $templates->getTemplateFile(Templates::Multiple), RendererException::class, 'Template file of type "multiple" not defined.', ); @@ -44,10 +44,10 @@ public function testTemplateFileShouldBeReturned(): void { $filename = realpath(__DIR__ . '/../resources/renderer/not-found/templates/not-found1.phtml'); $templates = new Templates([ - Templates::TemplateNotFound => $filename, + Templates::NotFound => $filename, ]); - Assert::same($filename, $templates->getTemplateFile(Templates::TemplateNotFound)); + Assert::same($filename, $templates->getTemplateFile(Templates::NotFound)); } public function testTemplatesShouldBeOverridden(): void @@ -58,21 +58,21 @@ public function testTemplatesShouldBeOverridden(): void $notFoundOverridden = realpath(__DIR__ . '/../resources/renderer/not-found/templates/not-found2.phtml'); $templates = new Templates([ - Templates::TemplateNotFound => $notFound, - Templates::TemplateSingle => $single, + Templates::NotFound => $notFound, + Templates::Single => $single, ]); $overriddenTemplates = $templates->override(new Templates([ - Templates::TemplateNotFound => $notFoundOverridden, + Templates::NotFound => $notFoundOverridden, ])); Assert::notSame($templates, $overriddenTemplates); - Assert::same($notFound, $templates->getTemplateFile(Templates::TemplateNotFound)); - Assert::same($single, $templates->getTemplateFile(Templates::TemplateSingle)); + Assert::same($notFound, $templates->getTemplateFile(Templates::NotFound)); + Assert::same($single, $templates->getTemplateFile(Templates::Single)); - Assert::same($notFoundOverridden, $overriddenTemplates->getTemplateFile(Templates::TemplateNotFound)); - Assert::same($single, $overriddenTemplates->getTemplateFile(Templates::TemplateSingle)); + Assert::same($notFoundOverridden, $overriddenTemplates->getTemplateFile(Templates::NotFound)); + Assert::same($single, $overriddenTemplates->getTemplateFile(Templates::Single)); } } diff --git a/tests/resources/renderer/client-side/data-provider.php b/tests/resources/renderer/client-side/data-provider.php new file mode 100644 index 0000000..4f792d3 --- /dev/null +++ b/tests/resources/renderer/client-side/data-provider.php @@ -0,0 +1,45 @@ + [ + new Position('homepage.top'), + [], + __DIR__ . '/positionOnly.html', + ], + 'With resources' => [ + new Position('homepage.top', [ + new BannerResource('role', 'vip'), + new BannerResource('category', [123, 456]), + ]), + [], + __DIR__ . '/withResources.html', + ], + 'With attributes' => [ + new Position('homepage.top'), + [ + 'class' => 'custom-class', + 'data-custom' => true, + 'data-custom2' => false, + 'data-custom3' => null, + ], + __DIR__ . '/withAttributes.html', + ], + 'With resources and attributes' => [ + new Position('homepage.top', [ + new BannerResource('role', 'vip'), + new BannerResource('category', [123, 456]), + ]), + [ + 'class' => 'custom-class', + 'data-custom' => true, + 'data-custom2' => false, + 'data-custom3' => null, + ], + __DIR__ . '/withResourcesAndAttributes.html', + ], +]; diff --git a/tests/resources/renderer/client-side/positionOnly.html b/tests/resources/renderer/client-side/positionOnly.html new file mode 100644 index 0000000..b5a6f0b --- /dev/null +++ b/tests/resources/renderer/client-side/positionOnly.html @@ -0,0 +1 @@ +
diff --git a/tests/resources/renderer/client-side/templates/client-side1.phtml b/tests/resources/renderer/client-side/templates/client-side1.phtml new file mode 100644 index 0000000..07e3bda --- /dev/null +++ b/tests/resources/renderer/client-side/templates/client-side1.phtml @@ -0,0 +1,10 @@ + +Client-side position "getCode()) ?>". diff --git a/tests/resources/renderer/client-side/withAttributes.html b/tests/resources/renderer/client-side/withAttributes.html new file mode 100644 index 0000000..3febf56 --- /dev/null +++ b/tests/resources/renderer/client-side/withAttributes.html @@ -0,0 +1 @@ +
diff --git a/tests/resources/renderer/client-side/withResources.html b/tests/resources/renderer/client-side/withResources.html new file mode 100644 index 0000000..074d5ad --- /dev/null +++ b/tests/resources/renderer/client-side/withResources.html @@ -0,0 +1 @@ +
diff --git a/tests/resources/renderer/client-side/withResourcesAndAttributes.html b/tests/resources/renderer/client-side/withResourcesAndAttributes.html new file mode 100644 index 0000000..dfb9490 --- /dev/null +++ b/tests/resources/renderer/client-side/withResourcesAndAttributes.html @@ -0,0 +1 @@ +