Skip to content

Commit

Permalink
Position Renderer
Browse files Browse the repository at this point in the history
- added services for rendering positions (banners)
- added default rendering bridge for `.phtml` template with the default templates for all types of positions
- added unit tests for renderer
  • Loading branch information
tg666 committed Nov 22, 2023
1 parent 362f586 commit f7089ca
Show file tree
Hide file tree
Showing 52 changed files with 2,024 additions and 1 deletion.
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
"guzzlehttp/guzzle": "^7.7"
},
"require-dev": {
"ext-dom": "*",
"ext-libxml": "*",
"friendsofphp/php-cs-fixer": "^3.17",
"kubawerlos/php-cs-fixer-custom-fixers": "^3.14",
"latte/latte": "^2.11",
"mockery/mockery": "^1.6",
"nette/bootstrap": "^3.1",
"nette/caching": "^3.1",
Expand All @@ -27,7 +30,8 @@
"psr/log": "^1.1",
"roave/security-advisories": "dev-latest",
"slope-it/clock-mock": "^0.4.0",
"symplify/phpstan-rules": "12.0.2.72"
"symplify/phpstan-rules": "12.0.2.72",
"wa72/html-pretty-min": "^0.2.0"
},
"config": {
"sort-packages": true
Expand Down
5 changes: 5 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ parameters:
level: 8
paths:
- src
ignoreErrors:
# ignore "unused" closure parameters for capturing the output buffer
-
message: '#Anonymous function has an unused use \$.+#'
path: src/Renderer/Phtml/PhtmlRendererBridge.php
53 changes: 53 additions & 0 deletions src/Exception/RendererException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace SixtyEightPublishers\AmpClient\Exception;

use RuntimeException;
use Throwable;
use function sprintf;

final class RendererException extends RuntimeException implements AmpExceptionInterface
{
public static function unableToCreateFingerprint(string $positionCode, string $bannerId, ?Throwable $previous = null): self
{
return new self(
sprintf(
'Unable to create fingerprint for banner %s on the position %s. %s',
$bannerId,
$positionCode,
null !== $previous ? $previous->getMessage() : '',
),
0,
$previous,
);
}

/**
* @param class-string $rendererBridgeClassname
*/
public static function rendererBridgeThrownError(string $rendererBridgeClassname, string $positionCode, Throwable $previous): self
{
return new self(
sprintf(
'Renderer bridge of type %s thrown an exception while rendering a position %s: %s',
$rendererBridgeClassname,
$positionCode,
$previous->getMessage(),
),
0,
$previous,
);
}

public static function templateFileNotFound(string $filename): self
{
return new self(
sprintf(
'Template file "%s" not found.',
$filename,
),
);
}
}
83 changes: 83 additions & 0 deletions src/Renderer/BannersResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace SixtyEightPublishers\AmpClient\Renderer;

use SixtyEightPublishers\AmpClient\Response\ValueObject\Banner;
use SixtyEightPublishers\AmpClient\Response\ValueObject\Position;
use function array_map;
use function array_search;
use function count;
use function max;
use function mt_getrandmax;
use function mt_rand;
use function usort;

class BannersResolver implements BannersResolverInterface
{
public function resolveSingle(Position $position): ?Banner
{
$banners = $position->getBanners();

if (0 >= count($banners)) {
return null;
}

$scores = array_map(
static fn (Banner $banner) => $banner->getScore(),
$banners,
);
$firstHighestScoreKey = array_search(max($scores), $scores, true);

return $banners[$firstHighestScoreKey] ?? null;
}

public function resolveRandom(Position $position): ?Banner
{
$banners = $position->getBanners();

if (0 >= count($banners)) {
return null;
}

$distributions = [];
$weightTotal = 0;

foreach ($banners as $banner) {
$weightTotal += $banner->getScore();
}

foreach ($banners as $index => $banner) {
$distributions[$index] = $banner->getScore() / $weightTotal;
}

$key = 0;
$selector = mt_rand() / mt_getrandmax();

while (0 < $selector) {
$selector -= $distributions[$key];
$key++;
}

$key--;

return $banners[$key] ?? $banners[0];
}

public function resolveMultiple(Position $position): array
{
$banners = $position->getBanners();

if (0 >= count($banners)) {
return [];
}

usort(
$banners,
static fn (Banner $left, Banner $right): int => $right->getScore() <=> $left->getScore(),
);

return $banners;
}
}
20 changes: 20 additions & 0 deletions src/Renderer/BannersResolverInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace SixtyEightPublishers\AmpClient\Renderer;

use SixtyEightPublishers\AmpClient\Response\ValueObject\Banner;
use SixtyEightPublishers\AmpClient\Response\ValueObject\Position;

interface BannersResolverInterface
{
public function resolveSingle(Position $position): ?Banner;

public function resolveRandom(Position $position): ?Banner;

/**
* @return array<int, Banner>
*/
public function resolveMultiple(Position $position): array;
}
136 changes: 136 additions & 0 deletions src/Renderer/BreakpointStyle/BreakpointStyle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

declare(strict_types=1);

namespace SixtyEightPublishers\AmpClient\Renderer\BreakpointStyle;

use SixtyEightPublishers\AmpClient\Renderer\Phtml\Helpers;
use SixtyEightPublishers\AmpClient\Response\ValueObject\Banner;
use SixtyEightPublishers\AmpClient\Response\ValueObject\Position;
use function array_map;
use function count;
use function implode;
use function krsort;
use function ksort;
use function sprintf;

final class BreakpointStyle
{
/** @var array<int, Selector> */
private array $selectors = [];

/** @var array<int, Media> */
private array $media = [];

public function __construct(Position $position, Banner $banner)
{
$selectorMask = sprintf(
'[data-amp-banner="%s"] [data-amp-content-breakpoint="%s"]',
Helpers::escapeHtmlAttr($position->getCode()),
'%s',
);

$defaultContent = null;
$alternativeContents = [];

foreach ($banner->getContents() as $content) {
if (null === $content->getBreakpoint()) {
$defaultContent = $content;
} else {
$alternativeContents[$content->getBreakpoint()] = $content;
}
}

if (Position::BreakpointTypeMax === $position->getBreakpointType()) {
$mediaRuleMask = 'max-width: %dpx';
krsort($alternativeContents);
} else {
$mediaRuleMask = 'min-width: %dpx';
ksort($alternativeContents);
}

foreach ($alternativeContents as $alternativeContent) {
$this->selectors[] = $selector = new Selector(sprintf(
$selectorMask,
$alternativeContent->getBreakpoint(),
));

$selector->properties[] = new Property('display', 'none');

$this->media[] = $media = new Media(sprintf(
$mediaRuleMask,
$alternativeContent->getBreakpoint(),
));

if (null !== $defaultContent) {
$media->selectors[] = $selectorInMedia = new Selector(sprintf(
$selectorMask,
'default',
));

$selectorInMedia->properties[] = new Property('display', 'none');
}

foreach ($alternativeContents as $alternativeContentInner) {
$media->selectors[] = $selectorInMedia = new Selector(sprintf(
$selectorMask,
$alternativeContentInner->getBreakpoint(),
));

$selectorInMedia->properties[] = new Property('display', $alternativeContentInner === $alternativeContent ? 'block' : 'none');
}
}
}

public function __toString(): string
{
return $this->getCss();
}

public function getCss(): string
{
if (0 >= count($this->selectors) && 0 >= count($this->media)) {
return '';
}

$styles = [];

foreach ($this->selectors as $selector) {
$styles[] = $this->stringifySelector($selector);
}

foreach ($this->media as $media) {
$styles[] = $this->stringifyMedia($media);
}

return '<style>' . implode('', $styles) . '</style>';
}

private function stringifySelector(Selector $selector): string
{
$properties = array_map(
static fn (Property $property): string => $property->name . ':' . $property->value,
$selector->properties,
);

return sprintf(
'%s{%s}',
$selector->selector,
implode(';', $properties),
);
}

private function stringifyMedia(Media $media): string
{
$selectors = array_map(
fn (Selector $selector): string => $this->stringifySelector($selector),
$media->selectors,
);

return sprintf(
'@media(%s){%s}',
$media->rule,
implode('', $selectors),
);
}
}
18 changes: 18 additions & 0 deletions src/Renderer/BreakpointStyle/Media.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace SixtyEightPublishers\AmpClient\Renderer\BreakpointStyle;

final class Media
{
public string $rule;

/** @var array<int, Selector> */
public array $selectors = [];

public function __construct(string $rule)
{
$this->rule = $rule;
}
}
18 changes: 18 additions & 0 deletions src/Renderer/BreakpointStyle/Property.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace SixtyEightPublishers\AmpClient\Renderer\BreakpointStyle;

final class Property
{
public string $name;

public string $value;

public function __construct(string $name, string $value)
{
$this->name = $name;
$this->value = $value;
}
}
18 changes: 18 additions & 0 deletions src/Renderer/BreakpointStyle/Selector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace SixtyEightPublishers\AmpClient\Renderer\BreakpointStyle;

final class Selector
{
public string $selector;

/** @var array<int, Property> */
public array $properties = [];

public function __construct(string $selector)
{
$this->selector = $selector;
}
}
Loading

0 comments on commit f7089ca

Please sign in to comment.