Refactor Template Events to use a standalone ComponentBlock with a bc manner

This commit is contained in:
Jacob Tobiasz 2023-10-02 10:05:46 +02:00
parent a1b7cd5c2d
commit 77af62ec7d
No known key found for this signature in database
GPG key ID: 3F84290201B006E0
47 changed files with 1375 additions and 494 deletions

View file

@ -233,7 +233,7 @@
"symfony/runtime": "^6.3.2",
"symfony/web-profiler-bundle": "^6.3.2",
"symplify/monorepo-builder": "^11.0",
"vimeo/psalm": "5.15.*",
"vimeo/psalm": "5.14.*",
"weirdan/doctrine-psalm-plugin": "^2.8"
},
"suggest": {

View file

@ -66,6 +66,7 @@
<DeprecatedInterface>
<errorLevel type="info">
<referencedClass name="ApiPlatform\Core\Api\IriConverterInterface" /> <!-- deprecated in ApiPlatform 2.7 -->
<referencedClass name="Sylius\Bundle\ShopBundle\Calculator\OrderItemsSubtotalCalculatorInterface" />
<referencedClass name="Sylius\Component\Core\Calculator\ProductVariantPriceCalculatorInterface" />
<referencedClass name="Sylius\Component\Core\Provider\ProductVariantsPricesProviderInterface" />
<referencedClass name="Sylius\Component\User\Security\UserPasswordEncoderInterface" />
@ -135,7 +136,7 @@
<InvalidPropertyAssignmentValue>
<errorLevel type="info">
<file name="src/Sylius/Bundle/UiBundle/DataCollector/TemplateBlockRenderingHistory.php" />
<file name="src/Sylius/Bundle/UiBundle/DataCollector/BlockRenderingHistory.php" />
</errorLevel>
</InvalidPropertyAssignmentValue>

View file

@ -13,8 +13,8 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Command;
use Sylius\Bundle\UiBundle\Registry\BlockRegistryInterface;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@ -25,7 +25,7 @@ final class DebugTemplateEventCommand extends Command
{
protected static $defaultName = 'sylius:debug:template-event';
public function __construct(private TemplateBlockRegistryInterface $templateBlockRegistry)
public function __construct(private BlockRegistryInterface $templateBlockRegistry)
{
parent::__construct();
}
@ -53,6 +53,9 @@ final class DebugTemplateEventCommand extends Command
$io->title(sprintf('Blocks registered for the template event "%s"', $eventName));
/** @var array<TemplateBlock> $templateBlocksForEvent */
$templateBlocksForEvent = $this->templateBlockRegistry->all()[$eventName] ?? [];
$io->table(
['Block name', 'Template', 'Priority', 'Enabled'],
array_map(
@ -62,7 +65,7 @@ final class DebugTemplateEventCommand extends Command
$templateBlock->getPriority(),
$templateBlock->isEnabled() ? 'TRUE' : 'FALSE',
],
$this->templateBlockRegistry->all()[$eventName] ?? [],
$templateBlocksForEvent,
),
);

View file

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\ContextProvider;
use Sylius\Bundle\UiBundle\ContextProvider\Exception\NoSupportedContextProvider;
use Sylius\Bundle\UiBundle\Registry\Block;
use Webmozart\Assert\Assert;
final class CompositeContextProvider implements ContextProviderInterface
{
/** @var array<ContextProviderInterface> */
private array $contextProviders;
/**
* @param iterable<ContextProviderInterface> $contextProviders
*/
public function __construct(iterable $contextProviders)
{
Assert::allIsInstanceOf($contextProviders, ContextProviderInterface::class);
$this->contextProviders = $contextProviders instanceof \Traversable ? iterator_to_array($contextProviders) : $contextProviders;
}
/**
* @param array<string, mixed> $templateContext
* @return array<string, mixed>
*/
public function provide(array $templateContext, Block $templateBlock): array
{
foreach ($this->contextProviders as $contextProvider) {
if (!$contextProvider->supports($templateBlock)) {
continue;
}
return $contextProvider->provide($templateContext, $templateBlock);
}
throw new NoSupportedContextProvider(sprintf('No supported context provider found for block "%s".', $templateBlock->getName()));
}
public function supports(Block $templateBlock): bool
{
return true;
}
}

View file

@ -13,11 +13,11 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\ContextProvider;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\Block;
interface ContextProviderInterface
{
public function provide(array $templateContext, TemplateBlock $templateBlock): array;
public function provide(array $templateContext, Block $templateBlock): array;
public function supports(TemplateBlock $templateBlock): bool;
public function supports(Block $templateBlock): bool;
}

View file

@ -13,16 +13,16 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\ContextProvider;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\Block;
final class DefaultContextProvider implements ContextProviderInterface
{
public function provide(array $templateContext, TemplateBlock $templateBlock): array
public function provide(array $templateContext, Block $templateBlock): array
{
return array_replace($templateBlock->getContext(), $templateContext);
}
public function supports(TemplateBlock $templateBlock): bool
public function supports(Block $templateBlock): bool
{
return true;
}

View file

@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\ContextProvider\Exception;
class NoSupportedContextProvider extends \RuntimeException
{
}

View file

@ -13,31 +13,41 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\DataCollector;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\Block;
/** @internal */
final class TemplateBlockRenderingHistory
final class BlockRenderingHistory
{
/** @psalm-var list<array{name: string, start: float, stop: float, time: float, blocks: list<array{definition: TemplateBlock, start: float, stop: float, time: float}>}> */
/** @psalm-var list<array{name: string, start: float, stop: float, time: float, blocks: list<array{definition: Block, start: float, stop: float, time: float}>}> */
private $renderedEvents = [];
/** @psalm-var list<array{name: string, start: float, stop?: float, time?: float, blocks: list<array{definition: TemplateBlock, start: float, stop: float, time: float}>}> */
/** @psalm-var list<array{name: string, start: float, stop?: float, time?: float, blocks: list<array{definition: Block, start: float, stop: float, time: float}>}> */
private array $currentlyRenderedEvents = [];
/** @psalm-var list<array{definition: TemplateBlock, start: float, stop?: float, time?: float}> */
/** @psalm-var list<array{definition: Block, start: float, stop?: float, time?: float}> */
private array $currentlyRenderedBlocks = [];
/**
* @param array<string> $eventNames
* @param array<string, mixed> $context
*/
public function startRenderingEvent(array $eventNames, array $context): void
{
$this->currentlyRenderedEvents[] = ['names' => $eventNames, 'start' => microtime(true), 'blocks' => []];
}
public function startRenderingBlock(TemplateBlock $templateBlock, array $context): void
/**
* @param array<string, mixed> $context
*/
public function startRenderingBlock(Block $templateBlock, array $context): void
{
$this->currentlyRenderedBlocks[] = ['definition' => $templateBlock, 'start' => microtime(true)];
}
public function stopRenderingBlock(TemplateBlock $templateBlock, array $context): void
/**
* @param array<string, mixed> $context
*/
public function stopRenderingBlock(Block $templateBlock, array $context): void
{
$currentlyRenderedBlock = array_pop($this->currentlyRenderedBlocks);
$currentlyRenderedBlock['stop'] = microtime(true);
@ -48,6 +58,10 @@ final class TemplateBlockRenderingHistory
$this->currentlyRenderedEvents[] = $currentlyRenderedEvent;
}
/**
* @param array<string> $eventNames
* @param array<string, mixed> $context
*/
public function stopRenderingEvent(array $eventNames, array $context): void
{
$currentlyRenderedEvent = array_pop($this->currentlyRenderedEvents);
@ -56,6 +70,9 @@ final class TemplateBlockRenderingHistory
$this->renderedEvents[] = $currentlyRenderedEvent;
}
/**
* @return array<array{name: string, start: float, stop: float, time: float, blocks: list<array{definition: Block, start: float, stop: float, time: float}>}>
*/
public function getRenderedEvents(): array
{
return $this->renderedEvents;

View file

@ -20,7 +20,7 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollector;
/** @internal */
final class TemplateBlockDataCollector extends DataCollector
{
public function __construct(private TemplateBlockRenderingHistory $templateBlockRenderingHistory)
public function __construct(private BlockRenderingHistory $templateBlockRenderingHistory)
{
$this->reset();
}

View file

@ -13,19 +13,19 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\DataCollector;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface;
use Sylius\Bundle\UiBundle\Registry\Block;
use Sylius\Bundle\UiBundle\Renderer\BlockRendererInterface;
/** @internal */
final class TraceableTemplateBlockRenderer implements TemplateBlockRendererInterface
final class TraceableBlockRenderer implements BlockRendererInterface
{
public function __construct(
private TemplateBlockRendererInterface $templateBlockRenderer,
private TemplateBlockRenderingHistory $templateBlockRenderingHistory,
private BlockRendererInterface $templateBlockRenderer,
private BlockRenderingHistory $templateBlockRenderingHistory,
) {
}
public function render(TemplateBlock $templateBlock, array $context = []): string
public function render(Block $templateBlock, array $context = []): string
{
$this->templateBlockRenderingHistory->startRenderingBlock($templateBlock, $context);

View file

@ -13,14 +13,14 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\DataCollector;
use Sylius\Bundle\UiBundle\Renderer\TemplateEventRendererInterface;
use Sylius\Bundle\UiBundle\Renderer\TwigEventRendererInterface;
/** @internal */
final class TraceableTemplateEventRenderer implements TemplateEventRendererInterface
final class TraceableTwigEventRenderer implements TwigEventRendererInterface
{
public function __construct(
private TemplateEventRendererInterface $templateEventRenderer,
private TemplateBlockRenderingHistory $templateBlockRenderingHistory,
private TwigEventRendererInterface $templateEventRenderer,
private BlockRenderingHistory $templateBlockRenderingHistory,
) {
}

View file

@ -42,11 +42,32 @@ final class Configuration implements ConfigurationInterface
->ifString()
->then(static fn (?string $template): array => ['template' => $template])
->end()
->validate()
->ifTrue(static function (array $block): bool {
if (!array_key_exists('template', $block) || !array_key_exists('component', $block)) {
return false;
}
return null !== $block['template'] && [] !== $block['component'];
})
->thenInvalid('You cannot use both "template" and "component" for a block.')
->end()
->children()
->booleanNode('enabled')->defaultNull()->end()
->arrayNode('context')->addDefaultsIfNotSet()->ignoreExtraKeys(false)->end()
->scalarNode('template')->defaultNull()->end()
->scalarNode('component')->defaultNull()->end()
->arrayNode('component')
->beforeNormalization()
->ifString()
->then(static fn (?string $component): array => ['name' => $component])
->end()
->treatNullLike([])
->addDefaultsIfNotSet()
->children()
->scalarNode('name')->isRequired()->end()
->arrayNode('inputs')->ignoreExtraKeys(false)->end()
->end()
->end()
->integerNode('priority')->defaultNull()->end()
;

View file

@ -14,8 +14,9 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\DependencyInjection;
use Laminas\Stdlib\SplPriorityQueue;
use Sylius\Bundle\UiBundle\Registry\BlockRegistryInterface;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
@ -44,7 +45,7 @@ final class SyliusUiExtension extends Extension
*/
private function loadEvents(array $eventsConfig, ContainerBuilder $container): void
{
$templateBlockRegistryDefinition = $container->findDefinition(TemplateBlockRegistryInterface::class);
$templateBlockRegistryDefinition = $container->findDefinition(BlockRegistryInterface::class);
$blocksForEvents = [];
foreach ($eventsConfig as $eventName => $eventConfiguration) {
@ -57,20 +58,53 @@ final class SyliusUiExtension extends Extension
$blocksPriorityQueue->insert($details, $details['priority'] ?? 0);
}
/** @psalm-var array{name: string, eventName: string, template: string, component: string|null, context: array, priority: int, enabled: bool} $details */
/** @psalm-var array{name: string, eventName: string, template: string, component: array{name?: string, inputs?: array<string, mixed>}, context: array, priority: int, enabled: bool} $details */
foreach ($blocksPriorityQueue->toArray() as $details) {
$blocksForEvents[$eventName][$details['name']] = new Definition(TemplateBlock::class, [
$details['name'],
$details['eventName'],
$details['template'],
$details['context'],
$details['priority'],
$details['enabled'],
$details['component'],
]);
$blockType = [] !== $details['component'] ? 'component' : 'template';
$blocksForEvents[$eventName][$details['name']] = match ($blockType) {
'template' => $this->createTemplateBlockDefinition($details),
'component' => $this->createComponentBlockDefinition($details),
};
}
}
$templateBlockRegistryDefinition->setArgument(0, $blocksForEvents);
}
/**
* @param array<string, mixed> $details
*/
private function createTemplateBlockDefinition(array $details): Definition
{
return new Definition(
TemplateBlock::class,
[
$details['name'],
$details['eventName'],
$details['template'],
$details['context'],
$details['priority'],
$details['enabled'],
],
);
}
/**
* @param array<string, mixed> $details
*/
private function createComponentBlockDefinition(array $details): Definition
{
return new Definition(
ComponentBlock::class,
[
$details['name'],
$details['eventName'],
$details['component']['name'],
$details['component']['inputs'] ?? [],
$details['context'],
$details['priority'],
$details['enabled'],
],
);
}
}

View file

@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Registry;
abstract class Block
{
/**
* @param array<string, mixed>|null $context
*/
public function __construct(
protected string $name,
protected string $eventName,
protected ?array $context,
protected ?int $priority,
protected ?bool $enabled,
) {
}
public function getName(): string
{
return $this->name;
}
public function getEventName(): string
{
return $this->eventName;
}
/**
* @return array<string, mixed>
*/
public function getContext(): array
{
return $this->context ?? [];
}
public function getPriority(): int
{
return $this->priority ?? 0;
}
public function isEnabled(): bool
{
return $this->enabled ?? true;
}
abstract public function overwriteWith(self $block): self;
}

View file

@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Registry;
use Laminas\Stdlib\SplPriorityQueue;
final class BlockRegistry implements BlockRegistryInterface
{
/**
* Blocks within an event should be sorted by their priority descending.
*
* @var array<string, array<string, Block>>
*/
private array $eventsToBlocks;
/**
* @param array<string, array<string, Block>> $eventsToTemplateBlocks
*/
public function __construct(array $eventsToTemplateBlocks)
{
$this->eventsToBlocks = $eventsToTemplateBlocks;
}
public function all(): array
{
return $this->eventsToBlocks;
}
public function findEnabledForEvents(array $eventNames): array
{
// No need to sort blocks again if there's only one event because we have them sorted already
if (count($eventNames) === 1) {
$eventName = reset($eventNames);
return array_values(array_filter(
$this->eventsToBlocks[$eventName] ?? [],
static fn (Block $block): bool => $block->isEnabled(),
));
}
$enabledFinalizedBlocks = array_filter(
$this->findFinalizedForEvents($eventNames),
static fn (Block $block): bool => $block->isEnabled(),
);
$blocksPriorityQueue = new SplPriorityQueue();
foreach ($enabledFinalizedBlocks as $block) {
$blocksPriorityQueue->insert($block, $block->getPriority());
}
return $blocksPriorityQueue->toArray();
}
/**
* @param string[] $eventNames
*
* @return Block[]
*/
private function findFinalizedForEvents(array $eventNames): array
{
/** @var array<string, Block> $finalizedBlocks */
$finalizedBlocks = [];
$reversedEventNames = array_reverse($eventNames);
foreach ($reversedEventNames as $eventName) {
$blocks = $this->eventsToBlocks[$eventName] ?? [];
foreach ($blocks as $blockName => $block) {
if (array_key_exists($blockName, $finalizedBlocks)) {
$block = $finalizedBlocks[$blockName]->overwriteWith($block);
}
$finalizedBlocks[$blockName] = $block;
}
}
return $finalizedBlocks;
}
}
class_alias(BlockRegistry::class, '\Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistry');

View file

@ -13,23 +13,19 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Registry;
interface TemplateBlockRegistryInterface
interface BlockRegistryInterface
{
/**
* @return TemplateBlock[][]
*
* @psalm-return array<string, array<string, TemplateBlock>>
* @return array<string, array<string, Block>>
*/
public function all(): array;
/**
* @param string[] $eventNames
*
* @psalm-param non-empty-list<string> $eventNames
*
* @return TemplateBlock[]
*
* @psalm-return list<TemplateBlock>
* @return Block[]
*/
public function findEnabledForEvents(array $eventNames): array;
}
class_alias(BlockRegistryInterface::class, '\Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface');

View file

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Registry;
final class ComponentBlock extends Block
{
/**
* @param array<string, mixed> $componentInputs
* @param array<string, mixed>|null $context
*/
public function __construct(
string $name,
string $eventName,
private string $componentName,
private array $componentInputs,
?array $context,
?int $priority,
?bool $enabled,
) {
parent::__construct($name, $eventName, $context, $priority, $enabled);
}
public function getComponentName(): string
{
return $this->componentName;
}
/** @return array<string> */
public function getComponentInputs(): array
{
return $this->componentInputs;
}
public function overwriteWith(Block $block): self
{
if (!$block instanceof ComponentBlock) {
throw new \DomainException(sprintf(
'Trying to overwrite component block "%s" with block of different type "%s".',
$this->name,
get_class($block),
));
}
if ($this->name !== $block->name) {
throw new \DomainException(sprintf(
'Trying to overwrite block "%s" with block "%s".',
$this->name,
$block->name,
));
}
return new self(
$this->name,
$block->eventName,
$block->getComponentName(),
$block->getComponentInputs(),
$block->context ?? $this->context,
$block->priority ?? $this->priority,
$block->enabled ?? $this->enabled,
);
}
}

View file

@ -13,27 +13,17 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Registry;
final class TemplateBlock
final class TemplateBlock extends Block
{
public function __construct(
private string $name,
private string $eventName,
string $name,
string $eventName,
private ?string $template,
private ?array $context,
private ?int $priority,
private ?bool $enabled,
private ?string $component = null,
?array $context,
?int $priority,
?bool $enabled,
) {
}
public function getName(): string
{
return $this->name;
}
public function getEventName(): string
{
return $this->eventName;
parent::__construct($name, $eventName, $context, $priority, $enabled);
}
public function getTemplate(): string
@ -49,28 +39,16 @@ final class TemplateBlock
return $this->template;
}
public function getComponent(): ?string
public function overwriteWith(Block $block): self
{
return $this->component;
}
if (!$block instanceof self) {
throw new \DomainException(sprintf(
'Trying to overwrite template block "%s" with block of different type "%s".',
$this->name,
get_class($block),
));
}
public function getContext(): array
{
return $this->context ?? [];
}
public function getPriority(): int
{
return $this->priority ?? 0;
}
public function isEnabled(): bool
{
return $this->enabled ?? true;
}
public function overwriteWith(self $block): self
{
if ($this->name !== $block->name) {
throw new \DomainException(sprintf(
'Trying to overwrite block "%s" with block "%s".',
@ -86,7 +64,6 @@ final class TemplateBlock
$block->context ?? $this->context,
$block->priority ?? $this->priority,
$block->enabled ?? $this->enabled,
$block->component ?? $this->component,
);
}
}

View file

@ -1,93 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Registry;
use Laminas\Stdlib\SplPriorityQueue;
final class TemplateBlockRegistry implements TemplateBlockRegistryInterface
{
/**
* Blocks within an event should be sorted by their priority descending.
*
* @var TemplateBlock[][]
*
* @psalm-var array<string, array<string, TemplateBlock>>
*/
private $eventsToTemplateBlocks;
public function __construct(array $eventsToTemplateBlocks)
{
$this->eventsToTemplateBlocks = $eventsToTemplateBlocks;
}
public function all(): array
{
return $this->eventsToTemplateBlocks;
}
public function findEnabledForEvents(array $eventNames): array
{
// No need to sort blocks again if there's only one event because we have them sorted already
if (count($eventNames) === 1) {
$eventName = reset($eventNames);
return array_values(array_filter(
$this->eventsToTemplateBlocks[$eventName] ?? [],
static fn (TemplateBlock $templateBlock): bool => $templateBlock->isEnabled(),
));
}
$enabledFinalizedTemplateBlocks = array_filter(
$this->findFinalizedForEvents($eventNames),
static fn (TemplateBlock $templateBlock): bool => $templateBlock->isEnabled(),
);
$templateBlocksPriorityQueue = new SplPriorityQueue();
foreach ($enabledFinalizedTemplateBlocks as $templateBlock) {
$templateBlocksPriorityQueue->insert($templateBlock, $templateBlock->getPriority());
}
return $templateBlocksPriorityQueue->toArray();
}
/**
* @param string[] $eventNames
*
* @psalm-param non-empty-list<string> $eventNames
*
* @return TemplateBlock[]
*/
private function findFinalizedForEvents(array $eventNames): array
{
/**
* @var TemplateBlock[] $finalizedTemplateBlocks
*
* @psalm-var array<string, TemplateBlock> $finalizedTemplateBlocks
*/
$finalizedTemplateBlocks = [];
$reversedEventNames = array_reverse($eventNames);
foreach ($reversedEventNames as $eventName) {
$templateBlocks = $this->eventsToTemplateBlocks[$eventName] ?? [];
foreach ($templateBlocks as $blockName => $templateBlock) {
if (array_key_exists($blockName, $finalizedTemplateBlocks)) {
$templateBlock = $finalizedTemplateBlocks[$blockName]->overwriteWith($templateBlock);
}
$finalizedTemplateBlocks[$blockName] = $templateBlock;
}
}
return $finalizedTemplateBlocks;
}
}

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer;
use Sylius\Bundle\UiBundle\Registry\Block;
interface BlockRendererInterface
{
/**
* @param array<string, mixed> $context
*/
public function render(Block $templateBlock, array $context = []): string;
}
class_alias(BlockRendererInterface::class, '\Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface');

View file

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer;
use Sylius\Bundle\UiBundle\Registry\Block;
use Sylius\Bundle\UiBundle\Renderer\Exception\NoSupportedBlockRenderer;
use Webmozart\Assert\Assert;
/** @internal */
final class CompositeBlockRenderer implements BlockRendererInterface
{
/** @var array<SupportableBlockRendererInterface> */
private array $blockRenderers;
/**
* @param iterable<SupportableBlockRendererInterface> $blockRenderers
*/
public function __construct(iterable $blockRenderers)
{
Assert::allIsInstanceOf($blockRenderers, SupportableBlockRendererInterface::class);
$this->blockRenderers = $blockRenderers instanceof \Traversable ? iterator_to_array($blockRenderers) : $blockRenderers;
}
public function render(Block $templateBlock, array $context = []): string
{
foreach ($this->blockRenderers as $blockRenderer) {
if (!$blockRenderer->supports($templateBlock)) {
continue;
}
return $blockRenderer->render($templateBlock, $context);
}
throw new NoSupportedBlockRenderer(sprintf('No supported block renderer found for "%s" block.', $templateBlock->getName()));
}
}

View file

@ -13,14 +13,18 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer;
use Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface;
use Sylius\Bundle\UiBundle\Registry\BlockRegistryInterface;
final class DelegatingTemplateEventRenderer implements TemplateEventRendererInterface
final class DelegatingTwigEventRenderer implements TwigEventRendererInterface
{
public function __construct(private TemplateBlockRegistryInterface $templateBlockRegistry, private TemplateBlockRendererInterface $templateBlockRenderer)
public function __construct(private BlockRegistryInterface $templateBlockRegistry, private BlockRendererInterface $templateBlockRenderer)
{
}
/**
* @param array<string> $eventNames
* @param array<string, mixed> $context
*/
public function render(array $eventNames, array $context = []): string
{
$templateBlocks = $this->templateBlockRegistry->findEnabledForEvents($eventNames);

View file

@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer\Exception;
class NoSupportedBlockRenderer extends \RuntimeException
{
}

View file

@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer;
use Sylius\Bundle\UiBundle\Registry\Block;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
final class HtmlDebugBlockRenderer implements BlockRendererInterface
{
public function __construct(private BlockRendererInterface $blockRenderer)
{
}
public function render(Block $templateBlock, array $context = []): string
{
if (!$this->shouldRenderHtmlDebug($templateBlock)) {
return $this->blockRenderer->render($templateBlock, $context);
}
$renderedParts = [];
switch (get_class($templateBlock)) {
case TemplateBlock::class:
$renderedParts[] = $this->getBeginBlockForTemplate($templateBlock);
break;
case ComponentBlock::class:
$renderedParts[] = $this->getBeginBlockForComponent($templateBlock);
break;
}
$renderedParts[] = $this->blockRenderer->render($templateBlock, $context);
$renderedParts[] = $this->getEndBlock($templateBlock);
return implode("\n", $renderedParts);
}
private function shouldRenderHtmlDebug(Block $block): bool
{
return match (get_class($block)) {
TemplateBlock::class => strrpos($block->getTemplate(), '.html.twig') !== false,
ComponentBlock::class => true,
default => false,
};
}
private function getBeginBlockForTemplate(Block $block): string
{
if (!$block instanceof TemplateBlock) {
throw new \InvalidArgumentException(sprintf('Block must be instance of "%s"', TemplateBlock::class));
}
return sprintf(
'<!-- BEGIN BLOCK | event name: "%s", block name: "%s", template: "%s", priority: %d -->',
$block->getEventName(),
$block->getName(),
$block->getTemplate(),
$block->getPriority(),
);
}
private function getBeginBlockForComponent(Block $block): string
{
if (!$block instanceof ComponentBlock) {
throw new \InvalidArgumentException(sprintf('Block must be instance of "%s"', ComponentBlock::class));
}
return sprintf(
'<!-- BEGIN BLOCK | event name: "%s", block name: "%s", component: "%s", priority: %d -->',
$block->getEventName(),
$block->getName(),
$block->getComponentName(),
$block->getPriority(),
);
}
private function getEndBlock(Block $templateBlock): string
{
return sprintf(
'<!-- END BLOCK | event name: "%s", block name: "%s" -->',
$templateBlock->getEventName(),
$templateBlock->getName(),
);
}
}
class_alias(HtmlDebugBlockRenderer::class, '\Sylius\Bundle\UiBundle\Renderer\HtmlDebugTemplateBlockRenderer');

View file

@ -1,77 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
final class HtmlDebugTemplateBlockRenderer implements TemplateBlockRendererInterface
{
public function __construct(private TemplateBlockRendererInterface $templateBlockRenderer)
{
}
public function render(TemplateBlock $templateBlock, array $context = []): string
{
$isTemplateBlockComponent = null !== $templateBlock->getComponent();
$shouldRenderHtmlDebug = $isTemplateBlockComponent || strrpos($templateBlock->getTemplate(), '.html.twig') !== false;
if (!$shouldRenderHtmlDebug) {
return $this->templateBlockRenderer->render($templateBlock, $context);
}
$renderedParts = [];
if ($isTemplateBlockComponent) {
$renderedParts[] = $this->getBeginBlockForComponent($templateBlock);
} else {
$renderedParts[] = $this->getBeginBlockForTemplate($templateBlock);
}
$renderedParts[] = $this->templateBlockRenderer->render($templateBlock, $context);
$renderedParts[] = $this->getEndBlock($templateBlock);
return implode("\n", $renderedParts);
}
private function getBeginBlockForTemplate(TemplateBlock $templateBlock): string
{
return sprintf(
'<!-- BEGIN BLOCK | event name: "%s", block name: "%s", template: "%s", priority: %d -->',
$templateBlock->getEventName(),
$templateBlock->getName(),
$templateBlock->getTemplate(),
$templateBlock->getPriority(),
);
}
private function getBeginBlockForComponent(TemplateBlock $templateBlock): string
{
return sprintf(
'<!-- BEGIN BLOCK | event name: "%s", block name: "%s", component: "%s", priority: %d -->',
$templateBlock->getEventName(),
$templateBlock->getName(),
$templateBlock->getComponent(),
$templateBlock->getPriority(),
);
}
private function getEndBlock(TemplateBlock $templateBlock): string
{
return sprintf(
'<!-- END BLOCK | event name: "%s", block name: "%s" -->',
$templateBlock->getEventName(),
$templateBlock->getName(),
);
}
}

View file

@ -13,20 +13,26 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer;
use Sylius\Bundle\UiBundle\Registry\Block;
use Sylius\Bundle\UiBundle\Registry\BlockRegistryInterface;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface;
final class HtmlDebugTemplateEventRenderer implements TemplateEventRendererInterface
final class HtmlDebugTwigEventRenderer implements TwigEventRendererInterface
{
public function __construct(
private TemplateEventRendererInterface $templateEventRenderer,
private TemplateBlockRegistryInterface $templateBlockRegistry,
private TwigEventRendererInterface $templateEventRenderer,
private BlockRegistryInterface $blockRegistry,
) {
}
/**
* @param array<string> $eventNames
* @param array<string, mixed> $context
*/
public function render(array $eventNames, array $context = []): string
{
$shouldRenderHtmlDebug = $this->shouldRenderHtmlDebug($this->templateBlockRegistry->findEnabledForEvents($eventNames));
$shouldRenderHtmlDebug = $this->shouldRenderHtmlDebug($this->blockRegistry->findEnabledForEvents($eventNames));
$renderedParts = [];
@ -50,24 +56,24 @@ final class HtmlDebugTemplateEventRenderer implements TemplateEventRendererInter
}
/**
* @param TemplateBlock[] $templateBlocks
* @param Block[] $blocks
*/
private function shouldRenderHtmlDebug(array $templateBlocks): bool
private function shouldRenderHtmlDebug(array $blocks): bool
{
return count($templateBlocks) === 0 || $this->hasAnyBlockWithComponentOrTemplate($templateBlocks);
}
/**
* @param array<TemplateBlock> $templateBlocks
*/
private function hasAnyBlockWithComponentOrTemplate(array $templateBlocks): bool
{
foreach ($templateBlocks as $templateBlock) {
if (null !== $templateBlock->getComponent() || strrpos($templateBlock->getTemplate(), '.html.twig') !== false) {
return true;
}
if (0 === count($blocks)) {
return true;
}
return false;
return 0 !== count(
array_filter($blocks, function (Block $block): bool {
return match (get_class($block)) {
TemplateBlock::class => strrpos($block->getTemplate(), '.html.twig') !== false,
ComponentBlock::class => true,
default => false,
};
}),
);
}
}
class_alias(HtmlDebugTwigEventRenderer::class, '\Sylius\Bundle\UiBundle\Renderer\HtmlDebugTemplateEventRenderer');

View file

@ -13,9 +13,9 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\Block;
interface TemplateBlockRendererInterface
interface SupportableBlockRendererInterface extends BlockRendererInterface
{
public function render(TemplateBlock $templateBlock, array $context = []): string;
public function supports(Block $block): bool;
}

View file

@ -13,27 +13,71 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\ContextProvider\ContextProviderInterface;
use Sylius\Bundle\UiBundle\Registry\Block;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\UX\TwigComponent\ComponentRendererInterface;
/** @internal */
final class TwigComponentBlockRenderer implements TemplateBlockRendererInterface
final class TwigComponentBlockRenderer implements SupportableBlockRendererInterface
{
public function __construct(
private TemplateBlockRendererInterface $decoratedRenderer,
private ComponentRendererInterface $componentRenderer,
private ContextProviderInterface $contextProvider,
private ExpressionLanguage $expressionLanguage,
) {
}
/**
* @param ComponentBlock $templateBlock
* @param array<string, mixed> $context
*/
public function render(TemplateBlock $templateBlock, array $context = []): string
public function render(Block $templateBlock, array $context = []): string
{
if (null === $templateBlock->getComponent()) {
return $this->decoratedRenderer->render($templateBlock, $context);
if (!$this->supports($templateBlock)) {
throw new \InvalidArgumentException(sprintf(
'Block "%s" is not supported by "%s".',
$templateBlock->getName(),
self::class,
));
}
return $this->componentRenderer->createAndRender($templateBlock->getComponent(), ['context' => $templateBlock->getContext() + $context]);
$context = $this->contextProvider->provide($context, $templateBlock);
$inputs = $this->mapArrayRecursively(function (mixed $value) use ($context) {
if (!is_string($value)) {
return $value;
}
if (!str_starts_with($value, 'expr:')) {
return $value;
}
return $this->expressionLanguage->evaluate(substr($value, 5), $context);
}, $templateBlock->getComponentInputs());
return $this->componentRenderer->createAndRender($templateBlock->getComponentName(), $inputs);
}
/**
* @param array<array-key, mixed> $array
* @return array<array-key, mixed>
*/
private function mapArrayRecursively(callable $callback, array $array): array
{
$result = [];
foreach ($array as $key => $value) {
$result[$key] = is_array($value)
? $this->mapArrayRecursively($callback, $value)
: $callback($value);
}
return $result;
}
public function supports(Block $block): bool
{
return is_a($block, ComponentBlock::class);
}
}

View file

@ -13,12 +13,13 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer;
interface TemplateEventRendererInterface
interface TwigEventRendererInterface
{
/**
* @param string[] $eventNames
*
* @psalm-param non-empty-list<string> $eventNames
* @param array<string, mixed> $context
*/
public function render(array $eventNames, array $context = []): string;
}
class_alias(TwigEventRendererInterface::class, '\Sylius\Bundle\UiBundle\Renderer\TemplateEventRendererInterface');

View file

@ -14,17 +14,32 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Renderer;
use Sylius\Bundle\UiBundle\ContextProvider\ContextProviderInterface;
use Sylius\Bundle\UiBundle\Registry\Block;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Twig\Environment;
final class TwigTemplateBlockRenderer implements TemplateBlockRendererInterface
final class TwigTemplateBlockRenderer implements SupportableBlockRendererInterface
{
/**
* @param iterable<ContextProviderInterface> $contextProviders
*/
public function __construct(private Environment $twig, private iterable $contextProviders)
{
}
public function render(TemplateBlock $templateBlock, array $context = []): string
/**
* @param TemplateBlock $templateBlock
*/
public function render(Block $templateBlock, array $context = []): string
{
if (!$this->supports($templateBlock)) {
throw new \InvalidArgumentException(sprintf(
'Block "%s" is not supported by "%s".',
$templateBlock->getName(),
self::class,
));
}
foreach ($this->contextProviders as $contextProvider) {
if (!$contextProvider instanceof ContextProviderInterface || !$contextProvider->supports($templateBlock)) {
continue;
@ -35,4 +50,9 @@ final class TwigTemplateBlockRenderer implements TemplateBlockRendererInterface
return $this->twig->render($templateBlock->getTemplate(), $context);
}
public function supports(Block $block): bool
{
return is_a($block, TemplateBlock::class);
}
}

View file

@ -13,30 +13,33 @@
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Sylius\Bundle\UiBundle\DataCollector\TemplateBlockRenderingHistory" />
<service id="Sylius\Bundle\UiBundle\DataCollector\BlockRenderingHistory" />
<service id="Sylius\Bundle\UiBundle\DataCollector\TemplateBlockDataCollector">
<argument type="service" id="Sylius\Bundle\UiBundle\DataCollector\TemplateBlockRenderingHistory" />
<argument type="service" id="Sylius\Bundle\UiBundle\DataCollector\BlockRenderingHistory" />
<tag name="data_collector" template="@SyliusUi/DataCollector/templateBlock.html.twig" id="sylius_ui.template_block" />
</service>
<service id="Sylius\Bundle\UiBundle\DataCollector\TraceableTemplateBlockRenderer" decorates="Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface">
<argument type="service" id="Sylius\Bundle\UiBundle\DataCollector\TraceableTemplateBlockRenderer.inner" />
<argument type="service" id="Sylius\Bundle\UiBundle\DataCollector\TemplateBlockRenderingHistory" />
<service id="Sylius\Bundle\UiBundle\DataCollector\TraceableBlockRenderer" decorates="sylius.ui.block_renderer.composite">
<argument type="service" id="Sylius\Bundle\UiBundle\DataCollector\TraceableBlockRenderer.inner" />
<argument type="service" id="Sylius\Bundle\UiBundle\DataCollector\BlockRenderingHistory" />
</service>
<service id="Sylius\Bundle\UiBundle\DataCollector\TraceableTemplateBlockRenderer" alias="Sylius\Bundle\UiBundle\DataCollector\TraceableBlockRenderer" />
<service id="Sylius\Bundle\UiBundle\DataCollector\TraceableTwigEventRenderer" decorates="Sylius\Bundle\UiBundle\Renderer\TwigEventRendererInterface">
<argument type="service" id=".inner" />
<argument type="service" id="Sylius\Bundle\UiBundle\DataCollector\BlockRenderingHistory" />
</service>
<service id="Sylius\Bundle\UiBundle\DataCollector\TraceableTemplateEventRenderer" decorates="Sylius\Bundle\UiBundle\Renderer\TemplateEventRendererInterface">
<argument type="service" id="Sylius\Bundle\UiBundle\DataCollector\TraceableTemplateEventRenderer.inner" />
<argument type="service" id="Sylius\Bundle\UiBundle\DataCollector\TemplateBlockRenderingHistory" />
<service id="Sylius\Bundle\UiBundle\Renderer\HtmlDebugBlockRenderer" decorates="sylius.ui.block_renderer.composite">
<argument type="service" id="Sylius\Bundle\UiBundle\Renderer\HtmlDebugBlockRenderer.inner" />
</service>
<service id="Sylius\Bundle\UiBundle\Renderer\HtmlDebugTemplateBlockRenderer" alias="Sylius\Bundle\UiBundle\Renderer\HtmlDebugBlockRenderer" />
<service id="Sylius\Bundle\UiBundle\Renderer\HtmlDebugTemplateBlockRenderer" decorates="Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface">
<argument type="service" id="Sylius\Bundle\UiBundle\Renderer\HtmlDebugTemplateBlockRenderer.inner" />
</service>
<service id="Sylius\Bundle\UiBundle\Renderer\HtmlDebugTemplateEventRenderer" decorates="Sylius\Bundle\UiBundle\Renderer\TemplateEventRendererInterface">
<argument type="service" id="Sylius\Bundle\UiBundle\Renderer\HtmlDebugTemplateEventRenderer.inner" />
<service id="Sylius\Bundle\UiBundle\Renderer\HtmlDebugTwigEventRenderer" decorates="Sylius\Bundle\UiBundle\Renderer\TwigEventRendererInterface">
<argument type="service" id=".inner" />
<argument type="service" id="Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface" />
</service>
<service id="Sylius\Bundle\UiBundle\Renderer\HtmlDebugTemplateEventRenderer" alias="Sylius\Bundle\UiBundle\Renderer\HtmlDebugTwigEventRenderer" />
</services>
</container>

View file

@ -13,24 +13,34 @@
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface" class="Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistry">
<service id="sylius.ui.block_renderer.composite" class="Sylius\Bundle\UiBundle\Renderer\CompositeBlockRenderer">
<argument type="tagged_iterator" tag="sylius.ui.block_renderer" />
</service>
<service id="Sylius\Bundle\UiBundle\Registry\BlockRegistryInterface" class="Sylius\Bundle\UiBundle\Registry\BlockRegistry">
<argument type="collection" />
</service>
<service id="Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface" alias="Sylius\Bundle\UiBundle\Registry\BlockRegistryInterface" />
<service id="Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface" class="Sylius\Bundle\UiBundle\Renderer\TwigTemplateBlockRenderer">
<service id="Sylius\Bundle\UiBundle\Renderer\BlockRendererInterface" class="Sylius\Bundle\UiBundle\Renderer\TwigTemplateBlockRenderer">
<argument type="service" id="twig" />
<argument type="tagged_iterator" tag="sylius.ui.template_event.context_provider" />
<tag name="sylius.ui.block_renderer" />
</service>
<service id="Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface" alias="Sylius\Bundle\UiBundle\Renderer\BlockRendererInterface" />
<service id="Sylius\Bundle\UiBundle\Renderer\TwigComponentBlockRenderer" decorates="Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface" decoration-priority="1024">
<argument type="service" id=".inner" />
<service id="Sylius\Bundle\UiBundle\Renderer\TwigComponentBlockRenderer">
<argument type="service" id="ux.twig_component.component_renderer" />
<argument type="service" id="Sylius\Bundle\UiBundle\ContextProvider\ContextProviderInterface" />
<argument type="service" id="sylius.expression_language" />
<tag name="sylius.ui.block_renderer" />
</service>
<service id="Sylius\Bundle\UiBundle\Renderer\TemplateEventRendererInterface" class="Sylius\Bundle\UiBundle\Renderer\DelegatingTemplateEventRenderer">
<service id="Sylius\Bundle\UiBundle\Renderer\TwigEventRendererInterface" class="Sylius\Bundle\UiBundle\Renderer\DelegatingTwigEventRenderer">
<argument type="service" id="Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface" />
<argument type="service" id="Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface" />
<argument type="service" id="sylius.ui.block_renderer.composite" />
</service>
<service id="Sylius\Bundle\UiBundle\Renderer\TemplateEventRendererInterface" alias="Sylius\Bundle\UiBundle\Renderer\TwigEventRendererInterface" />
<service id="Sylius\Bundle\UiBundle\Command\DebugTemplateEventCommand">
<argument type="service" id="Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface" />

View file

@ -213,6 +213,108 @@ final class ConfigurationTest extends TestCase
);
}
/** @test */
public function component_block_configuration_can_be_shortened_to_template_string_only(): void
{
$this->assertProcessedConfigurationEquals(
[
[
'events' => [
'event_name' => [
'blocks' => [
'block_name' => [
'component' => 'component_name',
],
],
],
],
],
],
[
'events' => [
'event_name' => [
'blocks' => [
'block_name' => [
'component' => [
'name' => 'component_name',
],
],
],
],
],
],
'events.*.blocks.*.component',
);
}
/** @test */
public function component_block_configuration_can_be_defined_with_inputs(): void
{
$this->assertProcessedConfigurationEquals(
[
[
'events' => [
'event_name' => [
'blocks' => [
'block_name' => [
'component' => [
'name' => 'component_name',
'inputs' => [
'foo' => 'bar',
'bar' => 'baz',
],
],
],
],
],
],
],
],
[
'events' => [
'event_name' => [
'blocks' => [
'block_name' => [
'component' => [
'name' => 'component_name',
'inputs' => [
'foo' => 'bar',
'bar' => 'baz',
],
],
],
],
],
],
],
'events.*.blocks.*.component',
);
}
/** @test */
public function template_and_component_cannot_be_defined_at_the_same_time(): void
{
$this->assertPartialConfigurationIsInvalid(
[
[
'events' => [
'event_name' => [
'blocks' => [
'block_name' => [
'template' => 'template.html.twig',
'component' => [
'name' => 'component_name',
],
],
],
],
],
],
],
'events.*.blocks.*.template',
);
}
protected function getConfiguration(): ConfigurationInterface
{
return new Configuration();

View file

@ -15,8 +15,9 @@ namespace Sylius\Bundle\UiBundle\Tests\DependencyInjection;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase;
use Sylius\Bundle\UiBundle\DependencyInjection\SyliusUiExtension;
use Sylius\Bundle\UiBundle\Registry\BlockRegistryInterface;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface;
use Symfony\Component\DependencyInjection\Definition;
final class SyliusUiExtensionTest extends AbstractExtensionTestCase
@ -30,6 +31,7 @@ final class SyliusUiExtensionTest extends AbstractExtensionTestCase
'first_event' => ['blocks' => [
'first_block' => ['template' => 'first.html.twig', 'context' => [], 'enabled' => true, 'priority' => 0],
'second_block' => ['template' => 'second.html.twig', 'context' => ['foo' => 'bar'], 'enabled' => true, 'priority' => 0],
'third_block' => ['component' => 'component_name', 'context' => [], 'enabled' => true, 'priority' => 0],
]],
'second_event' => ['blocks' => [
'another_block' => ['template' => 'another.html.twig', 'context' => [], 'enabled' => true, 'priority' => 0],
@ -37,15 +39,16 @@ final class SyliusUiExtensionTest extends AbstractExtensionTestCase
]]);
$this->assertContainerBuilderHasServiceDefinitionWithArgument(
TemplateBlockRegistryInterface::class,
BlockRegistryInterface::class,
0,
[
'first_event' => [
'first_block' => new Definition(TemplateBlock::class, ['first_block', 'first_event', 'first.html.twig', [], 0, true, null]),
'second_block' => new Definition(TemplateBlock::class, ['second_block', 'first_event', 'second.html.twig', ['foo' => 'bar'], 0, true, null]),
'first_block' => new Definition(TemplateBlock::class, ['first_block', 'first_event', 'first.html.twig', [], 0, true]),
'second_block' => new Definition(TemplateBlock::class, ['second_block', 'first_event', 'second.html.twig', ['foo' => 'bar'], 0, true]),
'third_block' => new Definition(ComponentBlock::class, ['third_block', 'first_event', 'component_name', [], [], 0, true]),
],
'second_event' => [
'another_block' => new Definition(TemplateBlock::class, ['another_block', 'second_event', 'another.html.twig', [], 0, true, null]),
'another_block' => new Definition(TemplateBlock::class, ['another_block', 'second_event', 'another.html.twig', [], 0, true]),
],
],
);
@ -66,13 +69,13 @@ final class SyliusUiExtensionTest extends AbstractExtensionTestCase
]]);
$this->assertContainerBuilderHasServiceDefinitionWithArgument(
TemplateBlockRegistryInterface::class,
BlockRegistryInterface::class,
0,
['event_name' => [
'first_block' => new Definition(TemplateBlock::class, ['first_block', 'event_name', 'first.html.twig', [], 5, true, null]),
'second_block' => new Definition(TemplateBlock::class, ['second_block', 'event_name', 'second.html.twig', [], 0, true, null]),
'third_block' => new Definition(TemplateBlock::class, ['third_block', 'event_name', 'third.html.twig', [], 0, true, null]),
'fourth_block' => new Definition(TemplateBlock::class, ['fourth_block', 'event_name', 'fourth.html.twig', [], -5, true, null]),
'first_block' => new Definition(TemplateBlock::class, ['first_block', 'event_name', 'first.html.twig', [], 5, true]),
'second_block' => new Definition(TemplateBlock::class, ['second_block', 'event_name', 'second.html.twig', [], 0, true]),
'third_block' => new Definition(TemplateBlock::class, ['third_block', 'event_name', 'third.html.twig', [], 0, true]),
'fourth_block' => new Definition(TemplateBlock::class, ['fourth_block', 'event_name', 'fourth.html.twig', [], -5, true]),
]],
);
}

View file

@ -14,16 +14,16 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Tests\Functional\src;
use Sylius\Bundle\UiBundle\ContextProvider\ContextProviderInterface;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\Block;
final class CustomContextProvider implements ContextProviderInterface
{
public function provide(array $templateContext, TemplateBlock $templateBlock): array
public function provide(array $templateContext, Block $templateBlock): array
{
return $templateContext + ['custom' => 'yolo'];
}
public function supports(TemplateBlock $templateBlock): bool
public function supports(Block $templateBlock): bool
{
return 'custom_context_provider' === $templateBlock->getEventName();
}

View file

@ -13,14 +13,14 @@ declare(strict_types=1);
namespace Sylius\Bundle\UiBundle\Twig;
use Sylius\Bundle\UiBundle\Renderer\TemplateEventRendererInterface;
use Sylius\Bundle\UiBundle\Renderer\TwigEventRendererInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Webmozart\Assert\Assert;
final class TemplateEventExtension extends AbstractExtension
{
public function __construct(private TemplateEventRendererInterface $templateEventRenderer)
public function __construct(private TwigEventRendererInterface $templateEventRenderer)
{
}

View file

@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\UiBundle\ContextProvider;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\UiBundle\ContextProvider\ContextProviderInterface;
use Sylius\Bundle\UiBundle\ContextProvider\Exception\NoSupportedContextProvider;
use Sylius\Bundle\UiBundle\Registry\Block;
use Webmozart\Assert\InvalidArgumentException;
final class CompositeContextProviderSpec extends ObjectBehavior
{
function it_is_a_context_provider(): void
{
$this->beConstructedWith([]);
$this->shouldImplement(ContextProviderInterface::class);
}
function it_throws_an_exception_when_any_of_passed_context_providers_is_not_a_context_provider(): void
{
$this->beConstructedWith([new \stdClass()]);
$this->shouldThrow(InvalidArgumentException::class)->duringInstantiation();
}
function it_throws_an_exception_when_no_context_provider_supports_a_given_block(
ContextProviderInterface $someContextProvider,
Block $someBlock,
): void {
$someBlock->getName()->willReturn('some_block');
$someContextProvider->supports($someBlock)->willReturn(false);
$this->beConstructedWith([$someContextProvider]);
$this->shouldThrow(new NoSupportedContextProvider('No supported context provider found for block "some_block".'))->during('provide', [[], $someBlock]);
}
function it_returns_always_true_for_supports(Block $someBlock): void
{
$this->beConstructedWith([]);
$this->supports($someBlock)->shouldReturn(true);
}
function it_returns_a_context_for_a_first_supported_context_provider(
ContextProviderInterface $someContextProvider,
ContextProviderInterface $anotherContextProvider,
ContextProviderInterface $yetAnotherContextProvider,
Block $someBlock,
): void {
$someBlock->getName()->willReturn('some_block');
$someContextProvider->supports($someBlock)->willReturn(false);
$anotherContextProvider->supports($someBlock)->willReturn(true);
$anotherContextProvider->provide(['foo' => 'bar'], $someBlock)->willReturn(['foo' => 'qux']);
$yetAnotherContextProvider->supports($someBlock)->shouldNotBeCalled();
$this->beConstructedWith([$someContextProvider, $anotherContextProvider]);
$this->provide(['foo' => 'bar'], $someBlock)->shouldReturn(['foo' => 'qux']);
}
}

View file

@ -14,19 +14,20 @@ declare(strict_types=1);
namespace spec\Sylius\Bundle\UiBundle\Registry;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\UiBundle\Registry\BlockRegistryInterface;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface;
final class TemplateBlockRegistrySpec extends ObjectBehavior
final class BlockRegistrySpec extends ObjectBehavior
{
function let(): void
{
$this->beConstructedWith([]);
}
function it_is_a_template_block_registry(): void
function it_is_a_block_registry(): void
{
$this->shouldImplement(TemplateBlockRegistryInterface::class);
$this->shouldImplement(BlockRegistryInterface::class);
}
function it_returns_all_template_blocks(): void
@ -38,26 +39,28 @@ final class TemplateBlockRegistrySpec extends ObjectBehavior
$this->all()->shouldReturn(['event' => ['block_name' => $templateBlock]]);
}
function it_returns_enabled_template_blocks_for_a_given_event(): void
function it_returns_enabled_blocks_for_a_given_event(): void
{
$firstTemplateBlock = new TemplateBlock('first_block', 'event', 'first.html.twig', [], 0, true, null);
$secondTemplateBlock = new TemplateBlock('second_block', 'event', 'second.html.twig', [], 10, false, null);
$thirdTemplateBlock = new TemplateBlock('third_block', 'event', 'third.html.twig', [], 50, true, null);
$firstBlock = new TemplateBlock('first_block', 'event', 'first.html.twig', [], 0, true, null);
$secondBlock = new TemplateBlock('second_block', 'event', 'second.html.twig', [], 10, false, null);
$thirdBlock = new TemplateBlock('third_block', 'event', 'third.html.twig', [], 50, true, null);
$fourthBlock = new ComponentBlock('fourth_block', 'event', 'fourth.html.twig', [], [], 0, null);
$this->beConstructedWith([
'event' => [
'first_block' => $firstTemplateBlock,
'second_block' => $secondTemplateBlock,
'first_block' => $firstBlock,
'second_block' => $secondBlock,
],
'another_event' => [
'third_block' => $thirdTemplateBlock,
'third_block' => $thirdBlock,
'fourth_block' => $fourthBlock,
],
]);
$this->findEnabledForEvents(['event'])->shouldReturn([$firstTemplateBlock]);
$this->findEnabledForEvents(['event'])->shouldReturn([$firstBlock]);
}
function it_returns_enabled_template_blocks_for_multiple_events(): void
function it_returns_enabled_blocks_for_multiple_events(): void
{
$this->beConstructedWith([
'generic_event' => ['block' => new TemplateBlock('block', 'generic_event', 'generic.html.twig', ['foo' => 'bar'], 0, true)],
@ -82,7 +85,7 @@ final class TemplateBlockRegistrySpec extends ObjectBehavior
]);
}
function it_returns_enabled_template_blocks_sorted_by_priority_for_multiple_events(): void
function it_returns_enabled_blocks_sorted_by_priority_for_multiple_events(): void
{
$this->beConstructedWith([
'generic_event' => [

View file

@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\UiBundle\Registry;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
final class ComponentBlockSpec extends ObjectBehavior
{
function it_represents_a_component_block(): void
{
$this->beConstructedWith('block_name', 'event_name', 'MyComponent', ['my' => 'input'], ['my' => 'context'], 10, false);
$this->getName()->shouldReturn('block_name');
$this->getEventName()->shouldReturn('event_name');
$this->getComponentName()->shouldReturn('MyComponent');
$this->getComponentInputs()->shouldReturn(['my' => 'input']);
$this->getContext()->shouldReturn(['my' => 'context']);
$this->getPriority()->shouldReturn(10);
$this->isEnabled()->shouldReturn(false);
}
function it_overwrites_a_component_block_with_an_another_component_block(): void
{
$this->beConstructedWith('block_name', 'event_name', 'MyComponent', ['my' => 'input'], ['my' => 'context'], 10, false);
$this
->overwriteWith(
new ComponentBlock(
'block_name',
'specific_event_name',
'MyAnotherComponent',
['my' => 'another_input'],
['my' => 'another_context'],
20,
true,
),
)
->shouldBeLike(
new ComponentBlock(
'block_name',
'specific_event_name',
'MyAnotherComponent',
['my' => 'another_input'],
['my' => 'another_context'],
20,
true,
),
)
;
}
function it_throws_an_exception_if_trying_to_overwrite_with_a_differently_named_block(): void
{
$this->beConstructedWith('block_name', 'event_name', 'MyComponent', ['my' => 'input'], ['my' => 'context'], 10, false);
$this->shouldThrow(\DomainException::class)->during(
'overwriteWith',
[
new ComponentBlock(
'another_block_name',
'specific_event_name',
'MyComponent',
[],
[],
0,
false,
),
],
);
}
function it_throws_an_exception_when_trying_to_override_template_block_with_template_block(): void
{
$this->beConstructedWith('block_name', 'event_name', 'MyComponent', ['my' => 'input'], ['my' => 'context'], 10, false);
$exceptionMessage = sprintf(
'Trying to overwrite component block "%s" with block of different type "%s".',
'block_name',
'Sylius\Bundle\UiBundle\Registry\TemplateBlock',
);
$this
->shouldThrow(new \DomainException($exceptionMessage))
->during('overwriteWith', [new TemplateBlock('block_name', 'specific_event_name', null, null, null, true)])
;
}
}

View file

@ -14,18 +14,18 @@ declare(strict_types=1);
namespace spec\Sylius\Bundle\UiBundle\Registry;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
final class TemplateBlockSpec extends ObjectBehavior
{
function it_represents_a_template_block(): void
{
$this->beConstructedWith('block_name', 'event_name', 'block.html.twig', ['foo' => 'bar'], 10, false, null);
$this->beConstructedWith('block_name', 'event_name', 'block.html.twig', ['foo' => 'bar'], 10, false);
$this->getName()->shouldReturn('block_name');
$this->getEventName()->shouldReturn('event_name');
$this->getTemplate()->shouldReturn('block.html.twig');
$this->getComponent()->shouldReturn(null);
$this->getContext()->shouldReturn(['foo' => 'bar']);
$this->getPriority()->shouldReturn(10);
$this->isEnabled()->shouldReturn(false);
@ -33,43 +33,59 @@ final class TemplateBlockSpec extends ObjectBehavior
function it_overwrites_a_template_block_with_an_another_template_block(): void
{
$this->beConstructedWith('block_name', 'event_name', 'block.html.twig', ['foo' => 'bar'], 10, false, null);
$this->beConstructedWith('block_name', 'event_name', 'block.html.twig', ['foo' => 'bar'], 10, false);
$this
->overwriteWith(new TemplateBlock('block_name', 'specific_event_name', 'another.html.twig', null, null, null, 'some_component'))
->shouldBeLike(new TemplateBlock('block_name', 'specific_event_name', 'another.html.twig', ['foo' => 'bar'], 10, false, 'some_component'))
->overwriteWith(new TemplateBlock('block_name', 'specific_event_name', 'another.html.twig', null, null, null))
->shouldBeLike(new TemplateBlock('block_name', 'specific_event_name', 'another.html.twig', ['foo' => 'bar'], 10, false))
;
$this
->overwriteWith(new TemplateBlock('block_name', 'specific_event_name', null, [], null, null, null))
->shouldBeLike(new TemplateBlock('block_name', 'specific_event_name', 'block.html.twig', [], 10, false, null))
->overwriteWith(new TemplateBlock('block_name', 'specific_event_name', null, [], null, null))
->shouldBeLike(new TemplateBlock('block_name', 'specific_event_name', 'block.html.twig', [], 10, false))
;
$this
->overwriteWith(new TemplateBlock('block_name', 'specific_event_name', null, null, -5, null, null))
->shouldBeLike(new TemplateBlock('block_name', 'specific_event_name', 'block.html.twig', ['foo' => 'bar'], -5, false, null))
->overwriteWith(new TemplateBlock('block_name', 'specific_event_name', null, null, -5, null))
->shouldBeLike(new TemplateBlock('block_name', 'specific_event_name', 'block.html.twig', ['foo' => 'bar'], -5, false))
;
$this
->overwriteWith(new TemplateBlock('block_name', 'specific_event_name', null, null, null, true, null))
->shouldBeLike(new TemplateBlock('block_name', 'specific_event_name', 'block.html.twig', ['foo' => 'bar'], 10, true, null))
->overwriteWith(new TemplateBlock('block_name', 'specific_event_name', null, null, null, true))
->shouldBeLike(new TemplateBlock('block_name', 'specific_event_name', 'block.html.twig', ['foo' => 'bar'], 10, true))
;
}
function it_throws_an_exception_if_trying_to_overwrite_with_a_differently_named_block(): void
{
$this->beConstructedWith('block_name', 'event_name', 'block.html.twig', ['foo' => 'bar'], 10, false, null);
$this->beConstructedWith('block_name', 'event_name', 'block.html.twig', ['foo' => 'bar'], 10, false);
$this->shouldThrow(\DomainException::class)->during('overwriteWith', [new TemplateBlock('different_name', 'specific_event_name', null, null, null, null, null)]);
$this->shouldThrow(\DomainException::class)->during('overwriteWith', [new TemplateBlock('different_name', 'specific_event_name', null, null, null, null)]);
}
function it_has_sensible_defaults(): void
{
$this->beConstructedWith('block_name', 'event_name', null, null, null, null, null);
$this->beConstructedWith('block_name', 'event_name', null, null, null, null);
$this->shouldThrow(\DomainException::class)->during('getTemplate');
$this->getContext()->shouldReturn([]);
$this->getPriority()->shouldReturn(0);
$this->isEnabled()->shouldReturn(true);
}
function it_throws_an_exception_when_trying_to_override_template_block_with_component_block(): void
{
$this->beConstructedWith('block_name', 'event_name', 'block.html.twig', ['foo' => 'bar'], 10, false);
$exceptionMessage = sprintf(
'Trying to overwrite template block "%s" with block of different type "%s".',
'block_name',
'Sylius\Bundle\UiBundle\Registry\ComponentBlock',
);
$this
->shouldThrow(new \DomainException($exceptionMessage))
->during('overwriteWith', [new ComponentBlock('block_name', 'specific_event_name', 'MyComponent', [], [], 0, false)])
;
}
}

View file

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\UiBundle\Renderer;
use InvalidArgumentException;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\UiBundle\Registry\Block;
use Sylius\Bundle\UiBundle\Renderer\BlockRendererInterface;
use Sylius\Bundle\UiBundle\Renderer\Exception\NoSupportedBlockRenderer;
use Sylius\Bundle\UiBundle\Renderer\SupportableBlockRendererInterface;
final class CompositeBlockRendererSpec extends ObjectBehavior
{
function it_is_a_block_renderer(): void
{
$this->beConstructedWith([]);
$this->shouldImplement(BlockRendererInterface::class);
}
function it_throws_an_exception_when_any_of_passed_block_renderers_is_not_a_block_renderer(): void
{
$this->beConstructedWith([new \stdClass()]);
$this->shouldThrow(InvalidArgumentException::class)->duringInstantiation();
}
function it_throws_an_exception_when_no_block_renderers_supports_a_given_block(
SupportableBlockRendererInterface $someBlockRenderer,
Block $someBlock,
): void {
$someBlock->getName()->willReturn('some_block');
$someBlockRenderer->supports($someBlock)->willReturn(false);
$this->beConstructedWith([$someBlockRenderer]);
$this
->shouldThrow(new NoSupportedBlockRenderer('No supported block renderer found for "some_block" block.'))
->during('render', [$someBlock, []])
;
}
function it_renders_a_block_using_a_first_supported_block_renderer(
SupportableBlockRendererInterface $someBlockRenderer,
SupportableBlockRendererInterface $anotherBlockRenderer,
SupportableBlockRendererInterface $yetAnotherBlockRenderer,
Block $someBlock,
): void {
$someBlock->getName()->willReturn('some_block');
$someBlockRenderer->supports($someBlock)->willReturn(false);
$anotherBlockRenderer->supports($someBlock)->willReturn(true);
$anotherBlockRenderer->render($someBlock, [])->willReturn('Rendered block');
$yetAnotherBlockRenderer->supports($someBlock)->shouldNotBeCalled();
$this->beConstructedWith([$someBlockRenderer, $anotherBlockRenderer, $yetAnotherBlockRenderer]);
$this->render($someBlock, [])->shouldReturn('Rendered block');
}
}

View file

@ -15,26 +15,26 @@ namespace spec\Sylius\Bundle\UiBundle\Renderer;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Bundle\UiBundle\Registry\BlockRegistryInterface;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface;
use Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface;
use Sylius\Bundle\UiBundle\Renderer\TemplateEventRendererInterface;
use Sylius\Bundle\UiBundle\Renderer\BlockRendererInterface;
use Sylius\Bundle\UiBundle\Renderer\TwigEventRendererInterface;
final class DelegatingTemplateEventRendererSpec extends ObjectBehavior
final class DelegatingTwigEventRendererSpec extends ObjectBehavior
{
function let(TemplateBlockRegistryInterface $templateBlockRegistry, TemplateBlockRendererInterface $templateBlockRenderer): void
function let(BlockRegistryInterface $templateBlockRegistry, BlockRendererInterface $templateBlockRenderer): void
{
$this->beConstructedWith($templateBlockRegistry, $templateBlockRenderer);
}
function it_is_a_template_event_renderer(): void
function it_is_a_twig_event_renderer(): void
{
$this->shouldImplement(TemplateEventRendererInterface::class);
$this->shouldImplement(TwigEventRendererInterface::class);
}
function it_renders_template_events(
TemplateBlockRegistryInterface $templateBlockRegistry,
TemplateBlockRendererInterface $templateBlockRenderer,
function it_renders_blocks_for_a_given_template(
BlockRegistryInterface $templateBlockRegistry,
BlockRendererInterface $templateBlockRenderer,
): void {
$firstTemplateBlock = new TemplateBlock('first_block', 'best_event_ever', 'firstBlock.txt.twig', [], 0, true, null);
$secondTemplateBlock = new TemplateBlock('second_block', 'best_event_ever', 'secondBlock.txt.twig', [], 0, true, null);
@ -48,8 +48,8 @@ final class DelegatingTemplateEventRendererSpec extends ObjectBehavior
}
function it_returns_an_empty_string_if_no_blocks_are_found_for_an_event(
TemplateBlockRegistryInterface $templateBlockRegistry,
TemplateBlockRendererInterface $templateBlockRenderer,
BlockRegistryInterface $templateBlockRegistry,
BlockRendererInterface $templateBlockRenderer,
): void {
$templateBlockRegistry->findEnabledForEvents(['best_event_ever'])->willReturn([]);

View file

@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\UiBundle\Renderer;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Renderer\BlockRendererInterface;
final class HtmlDebugBlockRendererSpec extends ObjectBehavior
{
function let(BlockRendererInterface $templateBlockRenderer): void
{
$this->beConstructedWith($templateBlockRenderer);
}
function it_is_a_block_renderer(): void
{
$this->shouldImplement(BlockRendererInterface::class);
}
function it_does_not_render_html_debug_comment_if_template_block_is_not_an_html_twig_file_path(
BlockRendererInterface $templateBlockRenderer,
): void {
$templateBlock = new TemplateBlock('block_name', 'event_name', 'template', [], 0, true);
$templateBlockRenderer->render($templateBlock, [])->willReturn('template');
$this->render($templateBlock)->shouldReturn('template');
}
function it_renders_html_debug_comment_if_template_block_is_an_html_twig_file_path(
BlockRendererInterface $templateBlockRenderer,
): void {
$templateBlock = new TemplateBlock('block_name', 'event_name', 'template.html.twig', [], 0, true);
$templateBlockRenderer->render($templateBlock, [])->willReturn('template');
$this->render($templateBlock)->shouldReturn(
'<!-- BEGIN BLOCK | event name: "event_name", block name: "block_name", template: "template.html.twig", priority: 0 -->' . "\n" .
'template' . "\n" .
'<!-- END BLOCK | event name: "event_name", block name: "block_name" -->',
);
}
function it_renders_html_debug_comment_if_the_block_is_a_component(
BlockRendererInterface $templateBlockRenderer,
): void {
$componentBlock = new ComponentBlock('block_name', 'event_name', 'Component', [], [], 0, true);
$templateBlockRenderer->render($componentBlock, ['some' => 'context'])->willReturn('template');
$this->render($componentBlock, ['some' => 'context'])->shouldReturn(
'<!-- BEGIN BLOCK | event name: "event_name", block name: "block_name", component: "Component", priority: 0 -->' . "\n" .
'template' . "\n" .
'<!-- END BLOCK | event name: "event_name", block name: "block_name" -->',
);
}
}

View file

@ -1,69 +0,0 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\UiBundle\Renderer;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface;
final class HtmlDebugTemplateBlockRendererSpec extends ObjectBehavior
{
function let(TemplateBlockRendererInterface $templateBlockRenderer): void
{
$this->beConstructedWith($templateBlockRenderer);
}
function it_is_a_template_block_renderer(): void
{
$this->shouldImplement(TemplateBlockRendererInterface::class);
}
function it_does_not_render_a_debug_html_comment_if_the_template_block_is_not_a_component_nor_a_twig_template(
TemplateBlockRendererInterface $templateBlockRenderer,
): void {
$templateBlock = new TemplateBlock('block_name', 'event_name', 'some_content', null, null, true, null);
$templateBlockRenderer->render($templateBlock, [])->willReturn('Rendered template');
$this->render($templateBlock, [])->shouldReturn('Rendered template');
}
function it_renders_a_debug_html_comment_if_the_template_block_has_a_configured_component(
TemplateBlockRendererInterface $templateBlockRenderer,
): void {
$templateBlock = new TemplateBlock('block_name', 'event_name', 'some_content', null, null, true, 'component_name');
$templateBlockRenderer->render($templateBlock, [])->willReturn('Rendered template');
$this->render($templateBlock, [])->shouldReturn(
'<!-- BEGIN BLOCK | event name: "event_name", block name: "block_name", component: "component_name", priority: 0 -->' . "\n" .
'Rendered template' . "\n" .
'<!-- END BLOCK | event name: "event_name", block name: "block_name" -->'
);
}
function it_renders_a_debug_html_comment_if_the_template_block_has_a_configured_twig_template(
TemplateBlockRendererInterface $templateBlockRenderer,
): void {
$templateBlock = new TemplateBlock('block_name', 'event_name', 'template.html.twig', [], null, true, null);
$templateBlockRenderer->render($templateBlock, [])->willReturn('Rendered template');
$this->render($templateBlock, [])->shouldReturn(
'<!-- BEGIN BLOCK | event name: "event_name", block name: "block_name", template: "template.html.twig", priority: 0 -->' . "\n" .
'Rendered template' . "\n" .
'<!-- END BLOCK | event name: "event_name", block name: "block_name" -->'
);
}
}

View file

@ -14,25 +14,26 @@ declare(strict_types=1);
namespace spec\Sylius\Bundle\UiBundle\Renderer;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\UiBundle\Registry\BlockRegistryInterface;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlockRegistryInterface;
use Sylius\Bundle\UiBundle\Renderer\TemplateEventRendererInterface;
use Sylius\Bundle\UiBundle\Renderer\TwigEventRendererInterface;
final class HtmlDebugTemplateEventRendererSpec extends ObjectBehavior
final class HtmlDebugTwigEventRendererSpec extends ObjectBehavior
{
function let(TemplateEventRendererInterface $templateEventRenderer, TemplateBlockRegistryInterface $templateBlockRegistry): void
function let(TwigEventRendererInterface $templateEventRenderer, BlockRegistryInterface $templateBlockRegistry): void
{
$this->beConstructedWith($templateEventRenderer, $templateBlockRegistry);
}
function it_is_a_template_event_renderer(): void
{
$this->shouldImplement(TemplateEventRendererInterface::class);
$this->shouldImplement(TwigEventRendererInterface::class);
}
function it_does_not_render_html_debug_comments_when_there_are_no_template_blocks_with_a_defined_component_nor_template(
TemplateEventRendererInterface $templateEventRenderer,
TemplateBlockRegistryInterface $templateBlockRegistry
TwigEventRendererInterface $templateEventRenderer,
BlockRegistryInterface $templateBlockRegistry,
): void {
$templateBlockRegistry->findEnabledForEvents(['event_name'])->willReturn([
new TemplateBlock('some_block_one', 'some_event', 'some content', null, null, true, null),
@ -44,8 +45,8 @@ final class HtmlDebugTemplateEventRendererSpec extends ObjectBehavior
}
function it_renders_html_debug_comment_when_no_template_block_passed(
TemplateEventRendererInterface $templateEventRenderer,
TemplateBlockRegistryInterface $templateBlockRegistry
TwigEventRendererInterface $templateEventRenderer,
BlockRegistryInterface $templateBlockRegistry,
): void {
$templateBlockRegistry->findEnabledForEvents(['event_name'])->willReturn([]);
@ -54,16 +55,16 @@ final class HtmlDebugTemplateEventRendererSpec extends ObjectBehavior
$this->render(['event_name'])->shouldReturn(
'<!-- BEGIN EVENT | event name: "event_name" -->' . "\n" .
'rendered_content' . "\n" .
'<!-- END EVENT | event name: "event_name" -->'
'<!-- END EVENT | event name: "event_name" -->',
);
}
function it_renders_html_debug_comment_when_at_least_one_block_has_a_configured_component(
TemplateEventRendererInterface $templateEventRenderer,
TemplateBlockRegistryInterface $templateBlockRegistry
TwigEventRendererInterface $templateEventRenderer,
BlockRegistryInterface $templateBlockRegistry,
): void {
$templateBlockRegistry->findEnabledForEvents(['event_name'])->willReturn([
new TemplateBlock('some_block_one', 'some_event', 'some content', null, null, true, 'SomeComponent'),
new ComponentBlock('some_block_one', 'some_event', 'Component', [], [], 0, true),
]);
$templateEventRenderer->render(['event_name'], [])->willReturn('rendered_content');
@ -71,16 +72,16 @@ final class HtmlDebugTemplateEventRendererSpec extends ObjectBehavior
$this->render(['event_name'])->shouldReturn(
'<!-- BEGIN EVENT | event name: "event_name" -->' . "\n" .
'rendered_content' . "\n" .
'<!-- END EVENT | event name: "event_name" -->'
'<!-- END EVENT | event name: "event_name" -->',
);
}
function it_renders_html_debug_comment_when_at_least_one_block_has_a_configured_twig_template(
TemplateEventRendererInterface $templateEventRenderer,
TemplateBlockRegistryInterface $templateBlockRegistry
TwigEventRendererInterface $templateEventRenderer,
BlockRegistryInterface $templateBlockRegistry,
): void {
$templateBlockRegistry->findEnabledForEvents(['event_name'])->willReturn([
new TemplateBlock('some_block_one', 'some_event', 'some_template.html.twig', null, null, true, null),
new TemplateBlock('some_block_one', 'some_event', 'template.html.twig', null, null, true),
]);
$templateEventRenderer->render(['event_name'], [])->willReturn('rendered_content');
@ -88,7 +89,7 @@ final class HtmlDebugTemplateEventRendererSpec extends ObjectBehavior
$this->render(['event_name'])->shouldReturn(
'<!-- BEGIN EVENT | event name: "event_name" -->' . "\n" .
'rendered_content' . "\n" .
'<!-- END EVENT | event name: "event_name" -->'
'<!-- END EVENT | event name: "event_name" -->',
);
}
}

View file

@ -14,51 +14,91 @@ declare(strict_types=1);
namespace spec\Sylius\Bundle\UiBundle\Renderer;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\UiBundle\ContextProvider\ContextProviderInterface;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\UX\TwigComponent\ComponentRendererInterface;
final class TwigComponentBlockRendererSpec extends ObjectBehavior
{
function let(TemplateBlockRendererInterface $decoratedRenderer, ComponentRendererInterface $componentRenderer): void
{
$this->beConstructedWith($decoratedRenderer, $componentRenderer);
}
function it_invokes_decorated_renderer_when_no_component_is_set(
TemplateBlockRendererInterface $decoratedRenderer,
): void {
$someTemplateBlock = new TemplateBlock(
'some_name',
'some_event_name',
'some_template',
['some' => 'context'],
0,
true,
null,
);
$decoratedRenderer->render($someTemplateBlock, ['some' => 'context'])->shouldBeCalled();
$this->render($someTemplateBlock, ['some' => 'context']);
}
function it_renders_a_component_when_component_is_set(
function let(
ComponentRendererInterface $componentRenderer,
ContextProviderInterface $contextProvider,
ExpressionLanguage $expressionLanguage,
): void {
$someTemplateBlock = new TemplateBlock(
'some_name',
'some_event_name',
'some_template',
['another' => 'value'],
$this->beConstructedWith(
$componentRenderer,
$contextProvider,
$expressionLanguage,
);
}
function it_returns_true_if_block_is_supported(): void
{
$templateBlock = new TemplateBlock('block_name', 'event_name', 'template.html.twig', [], 0, true);
$componentBlock = new ComponentBlock('block_name', 'event_name', 'Component', [], [], 0, true);
$this->supports($templateBlock)->shouldReturn(false);
$this->supports($componentBlock)->shouldReturn(true);
}
function it_throws_an_exception_when_trying_to_render_unsupported_block(): void
{
$templateBlock = new TemplateBlock('block_name', 'event_name', 'template.html.twig', [], 0, true);
$this
->shouldThrow(\InvalidArgumentException::class)
->during('render', [$templateBlock])
;
}
function it_renders_component_block(
ComponentRendererInterface $componentRenderer,
ContextProviderInterface $contextProvider,
ExpressionLanguage $expressionLanguage,
): void {
$componentBlock = new ComponentBlock(
'block_name',
'event_name',
'Component',
[
'foo' => 'bar',
'bar' => 'expr:foo',
'nested' => [
'foo' => 'expr:bar',
'bar' => 'expr:baz',
],
],
[],
0,
true,
'some_component',
);
$context = [
'foo' => 'bar',
'bar' => 'baz',
'baz' => 'qux',
];
$contextProvider->provide([], $componentBlock)->willReturn($context);
$expressionLanguage->evaluate('foo', $context)->willReturn('bar');
$expressionLanguage->evaluate('bar', $context)->willReturn('baz');
$expressionLanguage->evaluate('baz', $context)->willReturn('qux');
$componentRenderer
->createAndRender('some_component', ['context' => ['another' => 'value', 'some' => 'value']])
->willReturn('some_rendered_component')
->createAndRender('Component', [
'foo' => 'bar',
'bar' => 'bar',
'nested' => [
'foo' => 'baz',
'bar' => 'qux',
],
])
->willReturn('rendered_component')
;
$this->render($someTemplateBlock, ['some' => 'value'])->shouldReturn('some_rendered_component');
$this->render($componentBlock)->shouldReturn('rendered_component');
}
}

View file

@ -16,8 +16,9 @@ namespace spec\Sylius\Bundle\UiBundle\Renderer;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Bundle\UiBundle\ContextProvider\ContextProviderInterface;
use Sylius\Bundle\UiBundle\Registry\ComponentBlock;
use Sylius\Bundle\UiBundle\Registry\TemplateBlock;
use Sylius\Bundle\UiBundle\Renderer\TemplateBlockRendererInterface;
use Sylius\Bundle\UiBundle\Renderer\BlockRendererInterface;
use Twig\Environment;
final class TwigTemplateBlockRendererSpec extends ObjectBehavior
@ -35,7 +36,17 @@ final class TwigTemplateBlockRendererSpec extends ObjectBehavior
function it_is_a_template_block_renderer(): void
{
$this->shouldImplement(TemplateBlockRendererInterface::class);
$this->shouldImplement(BlockRendererInterface::class);
}
function it_throws_an_exception_when_trying_to_render_unsupported_block(): void
{
$templateBlock = new ComponentBlock('block_name', 'event_name', 'Component', [], [], 0, true);
$this
->shouldThrow(\InvalidArgumentException::class)
->during('render', [$templateBlock])
;
}
function it_renders_a_template_block(