diff --git a/docs/twig-hooks/getting-started.md b/docs/twig-hooks/getting-started.md index d65dcd19..e1c965dd 100644 --- a/docs/twig-hooks/getting-started.md +++ b/docs/twig-hooks/getting-started.md @@ -94,6 +94,7 @@ sylius_twig_hooks: enabled: true # whether the hookable is enabled context: [] # key-value pair that will be passed to the context bag configuration: [] # key-value pair that will be passed to the configuration bag + condition: '@=user != null' # expression evaluated at runtime; the hookable is rendered only when it returns true priority: 0 # priority, the higher the number, the earlier the hookable will be hooked ``` {% endcode %} @@ -111,8 +112,24 @@ sylius_twig_hooks: context: [] # key-value pair that will be passed to the context bag props: [] # key-value pair that will be passed to our component as props configuration: [] # key-value pair that will be passed to the configuration bag + condition: '@=user != null' # expression evaluated at runtime; the hookable is rendered only when it returns true priority: 0 # priority, the higher the number, the earlier the hookable will be hooked ``` {% endcode %} {% endtab %} {% endtabs %} + +**Conditions in hookables** + +Unlike the `enabled` flag — which is evaluated statically during the application boot — the `condition` option is an [Expression Language](https://symfony.com/doc/current/expression_language.html) expression that is evaluated at runtime, each time the hook is rendered. The hookable is rendered only when the expression returns `true`. Both the whole hook context and the `_context` variable are available within the expression, so both of these work: + +```yaml +condition: '@=user != null' +condition: '@=_context.user != null' +``` + +Because the expression is evaluated with the whole hook context and the `_context` variable, this behaves like [Twig's `defined`](https://twig.symfony.com/doc/3.x/tests/defined.html)-style checks: `@=_context.product` returns `null` when the variable is absent from the context, so the hookable is simply not rendered. Referencing a missing variable directly (e.g. `@=product ...`) throws a clear `InvalidExpressionException` instead, which makes invalid conditions fail fast during development. + +{% hint style="info" %} +On Symfony 7.1 and newer, conditions are validated at the application boot: an invalid expression is reported as soon as the container is compiled. On older versions, errors are only reported at runtime, when the hook is rendered. +{% endhint %} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fa60158f..6a8b362b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -53,3 +53,8 @@ parameters: identifier: function.alreadyNarrowedType count: 2 path: src/TwigHooks/src/Twig/TokenParser/HookTokenParser.php + + - + message: '#^Method Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage\:\:lint\(\) invoked with 3 parameters, 2 required\.$#' + count: 1 + path: src/TwigHooks/src/DependencyInjection/SyliusTwigHooksExtension.php diff --git a/src/TwigHooks/config/services.php b/src/TwigHooks/config/services.php index 33953965..a5d723a5 100644 --- a/src/TwigHooks/config/services.php +++ b/src/TwigHooks/config/services.php @@ -18,6 +18,8 @@ use Sylius\TwigHooks\Hook\Normalizer\Prefix\CompositePrefixNormalizer; use Sylius\TwigHooks\Hook\Normalizer\Prefix\PrefixNormalizerInterface; use Sylius\TwigHooks\Hook\Normalizer\Prefix\RemoveSectionPartNormalizer; +use Sylius\TwigHooks\Hookable\Checker\HookableConditionChecker; +use Sylius\TwigHooks\Hookable\Checker\HookableConditionCheckerInterface; use Sylius\TwigHooks\Hookable\Metadata\HookableMetadataFactory; use Sylius\TwigHooks\Provider\ComponentPropsProvider; use Sylius\TwigHooks\Provider\DefaultConfigurationProvider; @@ -53,6 +55,13 @@ ; $services->alias(TemplateConfigurationProviderInterface::class, 'sylius_twig_hooks.provider.template_configuration'); + $services->set('sylius_twig_hooks.checker.hookable_condition', HookableConditionChecker::class) + ->args([ + inline_service(ExpressionLanguage::class), + ]) + ; + $services->alias(HookableConditionCheckerInterface::class, 'sylius_twig_hooks.checker.hookable_condition'); + $services->set('sylius_twig_hooks.registry.hookables', HookablesRegistry::class) ->args([ tagged_iterator('sylius_twig_hooks.hookable'), diff --git a/src/TwigHooks/config/services/hook_renderer.php b/src/TwigHooks/config/services/hook_renderer.php index 90b83e0f..878ea3c1 100644 --- a/src/TwigHooks/config/services/hook_renderer.php +++ b/src/TwigHooks/config/services/hook_renderer.php @@ -26,6 +26,7 @@ service('sylius_twig_hooks.provider.default_context'), service('sylius_twig_hooks.provider.default_configuration'), service('sylius_twig_hooks.factory.hookable_metadata'), + service('sylius_twig_hooks.checker.hookable_condition'), ]) ->alias(HookRendererInterface::class, 'sylius_twig_hooks.renderer.hook') ; diff --git a/src/TwigHooks/src/DependencyInjection/Configuration.php b/src/TwigHooks/src/DependencyInjection/Configuration.php index 35e652d1..e1af7f74 100644 --- a/src/TwigHooks/src/DependencyInjection/Configuration.php +++ b/src/TwigHooks/src/DependencyInjection/Configuration.php @@ -129,6 +129,24 @@ private function addHooksConfiguration(ArrayNodeDefinition $rootNode): void ->prototype('variable')->end() ->end() ->integerNode('priority')->defaultNull()->end() + ->scalarNode('condition') + ->defaultNull() + ->validate() + ->always(function ($value) { + if (null === $value) { + return $value; + } + + if (!is_string($value) || !str_starts_with($value, '@=')) { + throw new \InvalidArgumentException( + sprintf('The "condition" value must be an expression prefixed with "@=". Got "%s".', get_debug_type($value)), + ); + } + + return $value; + }) + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/TwigHooks/src/DependencyInjection/SyliusTwigHooksExtension.php b/src/TwigHooks/src/DependencyInjection/SyliusTwigHooksExtension.php index 1305465f..ec4f8bac 100644 --- a/src/TwigHooks/src/DependencyInjection/SyliusTwigHooksExtension.php +++ b/src/TwigHooks/src/DependencyInjection/SyliusTwigHooksExtension.php @@ -16,10 +16,13 @@ use Sylius\TwigHooks\Hookable\DisabledHookable; use Sylius\TwigHooks\Hookable\HookableComponent; use Sylius\TwigHooks\Hookable\HookableTemplate; +use Sylius\TwigHooks\Provider\Exception\InvalidExpressionException; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\Parser; final class SyliusTwigHooksExtension extends Extension { @@ -46,12 +49,16 @@ public function load(array $configs, ContainerBuilder $container): void */ private function registerHooks(ContainerBuilder $container, array $hooks, array $supportedHookableTypes): void { + $expressionLanguage = new ExpressionLanguage(); + foreach ($hooks as $hookName => $hookables) { foreach ($hookables as $hookableName => $hookable) { if (!array_key_exists($hookable['type'], $supportedHookableTypes)) { throw new \InvalidArgumentException(sprintf('Hookable type "%s" is not supported.', $hookable['type'])); } + $this->lintCondition($expressionLanguage, $hookName, $hookableName, $hookable); + $this->registerHookable( $container, $supportedHookableTypes[$hookable['type']], @@ -63,6 +70,33 @@ private function registerHooks(ContainerBuilder $container, array $hooks, array } } + /** + * @param array $hookable + */ + private function lintCondition(ExpressionLanguage $expressionLanguage, string $hookName, string $hookableName, array $hookable): void + { + $condition = $hookable['condition'] ?? null; + + if (null === $condition || !defined(Parser::class . '::IGNORE_UNKNOWN_VARIABLES')) { + return; + } + + try { + $expressionLanguage->lint(substr($condition, 2), [], Parser::IGNORE_UNKNOWN_VARIABLES); + } catch (\Throwable $e) { + throw new InvalidExpressionException( + sprintf( + 'Failed to lint the "%s" condition of the "%s" hookable in the "%s" hook. Error: %s".', + $condition, + $hookableName, + $hookName, + $e->getMessage(), + ), + previous: $e, + ); + } + } + /** * @param array $hookable */ @@ -99,7 +133,7 @@ private function registerTemplateHookable( $hookable['context'], $hookable['configuration'], $hookable['priority'], - $hookable['enabled'], + $hookable['condition'], ]) ->addTag('sylius_twig_hooks.hookable', ['priority' => $hookable['priority']]) ; @@ -124,7 +158,7 @@ private function registerComponentHookable( $hookable['context'], $hookable['configuration'], $hookable['priority'], - $hookable['enabled'], + $hookable['condition'], ]) ->addTag('sylius_twig_hooks.hookable', ['priority' => $hookable['priority']]) ; diff --git a/src/TwigHooks/src/Hook/Renderer/HookRenderer.php b/src/TwigHooks/src/Hook/Renderer/HookRenderer.php index 63db812a..f68c326d 100644 --- a/src/TwigHooks/src/Hook/Renderer/HookRenderer.php +++ b/src/TwigHooks/src/Hook/Renderer/HookRenderer.php @@ -16,6 +16,7 @@ use Sylius\TwigHooks\Bag\DataBag; use Sylius\TwigHooks\Bag\ScalarDataBag; use Sylius\TwigHooks\Hook\Metadata\HookMetadata; +use Sylius\TwigHooks\Hookable\Checker\HookableConditionCheckerInterface; use Sylius\TwigHooks\Hookable\Metadata\HookableMetadataFactoryInterface; use Sylius\TwigHooks\Hookable\Renderer\HookableRendererInterface; use Sylius\TwigHooks\Provider\ConfigurationProviderInterface; @@ -30,6 +31,7 @@ public function __construct( private readonly ContextProviderInterface $contextProvider, private readonly ConfigurationProviderInterface $configurationProvider, private readonly HookableMetadataFactoryInterface $hookableMetadataFactory, + private readonly HookableConditionCheckerInterface $hookableConditionChecker, ) { } @@ -46,6 +48,11 @@ public function render(array $hookNames, array $hookContext = []): string $hookMetadata = new HookMetadata($hookable->hookName, new DataBag($hookContext)); $context = $this->contextProvider->provide($hookable, $hookContext); + + if (!$this->hookableConditionChecker->isEnabled($hookable, $context)) { + continue; + } + $configuration = $this->configurationProvider->provide($hookable); $hookableMetadata = $this->hookableMetadataFactory->create( diff --git a/src/TwigHooks/src/Hookable/AbstractHookable.php b/src/TwigHooks/src/Hookable/AbstractHookable.php index 7cfe8592..21e0831a 100644 --- a/src/TwigHooks/src/Hookable/AbstractHookable.php +++ b/src/TwigHooks/src/Hookable/AbstractHookable.php @@ -27,6 +27,8 @@ abstract class AbstractHookable /** @var array */ public readonly array $configuration; + public readonly ?string $condition; + private readonly ?int $priority; public const DEFAULT_PRIORITY = 0; @@ -41,6 +43,7 @@ public function __construct( array $context = [], array $configuration = [], ?int $priority = null, + ?string $condition = null, ) { $this->id = sprintf('%s#%s', $hookName, $name); $this->hookName = $hookName; @@ -48,6 +51,7 @@ public function __construct( $this->context = $context; $this->configuration = $configuration; $this->priority = $priority; + $this->condition = $condition; } public function priority(): int diff --git a/src/TwigHooks/src/Hookable/Checker/HookableConditionChecker.php b/src/TwigHooks/src/Hookable/Checker/HookableConditionChecker.php new file mode 100644 index 00000000..9a691b79 --- /dev/null +++ b/src/TwigHooks/src/Hookable/Checker/HookableConditionChecker.php @@ -0,0 +1,56 @@ + $context + */ + public function isEnabled(AbstractHookable $hookable, array $context): bool + { + $condition = $hookable->condition; + + if (null === $condition) { + return true; + } + + $values = array_merge($context, ['_context' => new DataBag($context)]); + + try { + return (bool) $this->expressionLanguage->evaluate(substr($condition, 2), $values); + } catch (\Throwable $e) { + throw new InvalidExpressionException( + sprintf( + 'Failed to evaluate the "%s" condition while rendering the "%s" hookable in the "%s" hook. Error: %s".', + $condition, + $hookable->name, + $hookable->hookName, + $e->getMessage(), + ), + previous: $e, + ); + } + } +} diff --git a/src/TwigHooks/src/Hookable/Checker/HookableConditionCheckerInterface.php b/src/TwigHooks/src/Hookable/Checker/HookableConditionCheckerInterface.php new file mode 100644 index 00000000..b1b8c2bc --- /dev/null +++ b/src/TwigHooks/src/Hookable/Checker/HookableConditionCheckerInterface.php @@ -0,0 +1,24 @@ + $context + */ + public function isEnabled(AbstractHookable $hookable, array $context): bool; +} diff --git a/src/TwigHooks/src/Hookable/DisabledHookable.php b/src/TwigHooks/src/Hookable/DisabledHookable.php index 2c5afc4a..604ed023 100644 --- a/src/TwigHooks/src/Hookable/DisabledHookable.php +++ b/src/TwigHooks/src/Hookable/DisabledHookable.php @@ -21,8 +21,9 @@ public function __construct( array $context = [], array $configuration = [], ?int $priority = null, + ?string $condition = null, ) { - parent::__construct($hookName, $name, $context, $configuration, $priority); + parent::__construct($hookName, $name, $context, $configuration, $priority, $condition); } public function toArray(): array @@ -32,6 +33,7 @@ public function toArray(): array 'name' => $this->name, 'context' => $this->context, 'configuration' => $this->configuration, + 'condition' => $this->condition, 'priority' => $this->priority(), ]; } diff --git a/src/TwigHooks/src/Hookable/HookableComponent.php b/src/TwigHooks/src/Hookable/HookableComponent.php index 207cd8bf..aa2352c2 100644 --- a/src/TwigHooks/src/Hookable/HookableComponent.php +++ b/src/TwigHooks/src/Hookable/HookableComponent.php @@ -28,8 +28,9 @@ public function __construct( array $context = [], array $configuration = [], ?int $priority = null, + ?string $condition = null, ) { - parent::__construct($hookName, $name, $context, $configuration, $priority); + parent::__construct($hookName, $name, $context, $configuration, $priority, $condition); } public function toArray(): array @@ -41,6 +42,7 @@ public function toArray(): array 'props' => $this->props, 'context' => $this->context, 'configuration' => $this->configuration, + 'condition' => $this->condition, 'priority' => $this->priority(), ]; } diff --git a/src/TwigHooks/src/Hookable/HookableTemplate.php b/src/TwigHooks/src/Hookable/HookableTemplate.php index a87ad91b..6eb448b0 100644 --- a/src/TwigHooks/src/Hookable/HookableTemplate.php +++ b/src/TwigHooks/src/Hookable/HookableTemplate.php @@ -22,8 +22,9 @@ public function __construct( array $context = [], array $configuration = [], ?int $priority = null, + ?string $condition = null, ) { - parent::__construct($hookName, $name, $context, $configuration, $priority); + parent::__construct($hookName, $name, $context, $configuration, $priority, $condition); } public function toArray(): array @@ -34,6 +35,7 @@ public function toArray(): array 'template' => $this->template, 'context' => $this->context, 'configuration' => $this->configuration, + 'condition' => $this->condition, 'priority' => $this->priority(), ]; } diff --git a/src/TwigHooks/tests/Functional/.application/config/packages/twig_hooks.yaml b/src/TwigHooks/tests/Functional/.application/config/packages/twig_hooks.yaml index de8e5235..7a22c667 100644 --- a/src/TwigHooks/tests/Functional/.application/config/packages/twig_hooks.yaml +++ b/src/TwigHooks/tests/Functional/.application/config/packages/twig_hooks.yaml @@ -35,3 +35,11 @@ sylius_twig_hooks: 'hookable_twig_component': dummy: component: 'app:dummy' + + 'enabled_by_condition.index': + with_condition_true: + template: 'enabled_by_condition/index/block.html.twig' + condition: '@=show === true' + with_condition_false: + template: 'enabled_by_condition/index/block.html.twig' + condition: '@=show === false' diff --git a/src/TwigHooks/tests/Functional/.application/templates/enabled_by_condition/index.html.twig b/src/TwigHooks/tests/Functional/.application/templates/enabled_by_condition/index.html.twig new file mode 100644 index 00000000..4ab80e5d --- /dev/null +++ b/src/TwigHooks/tests/Functional/.application/templates/enabled_by_condition/index.html.twig @@ -0,0 +1,3 @@ +{% hook 'enabled_by_condition.index' with { + show: show, +} only %} diff --git a/src/TwigHooks/tests/Functional/.application/templates/enabled_by_condition/index/block.html.twig b/src/TwigHooks/tests/Functional/.application/templates/enabled_by_condition/index/block.html.twig new file mode 100644 index 00000000..61fe1569 --- /dev/null +++ b/src/TwigHooks/tests/Functional/.application/templates/enabled_by_condition/index/block.html.twig @@ -0,0 +1 @@ +Condition block rendered. diff --git a/src/TwigHooks/tests/Functional/Twig/EnabledHookableConditionTest.php b/src/TwigHooks/tests/Functional/Twig/EnabledHookableConditionTest.php new file mode 100644 index 00000000..18ff28b1 --- /dev/null +++ b/src/TwigHooks/tests/Functional/Twig/EnabledHookableConditionTest.php @@ -0,0 +1,51 @@ +render('enabled_by_condition/index.html.twig', ['show' => true]); + + $this->assertStringContainsString('with_condition_true', $result); + $this->assertStringContainsString('Condition block rendered.', $result); + $this->assertStringNotContainsString('with_condition_false', $result); + } + + public function testItSkipsHookableWhenConditionIsNotSatisfied(): void + { + $result = $this->render('enabled_by_condition/index.html.twig', ['show' => false]); + + $this->assertStringContainsString('with_condition_false', $result); + $this->assertStringNotContainsString('with_condition_true', $result); + } + + /** + * @param array $variables + */ + private function render(string $path, array $variables = []): string + { + /** @var Twig $twig */ + $twig = $this->getContainer()->get('twig'); + + return $twig->render($path, $variables); + } +} diff --git a/src/TwigHooks/tests/Integration/DependencyInjection/ConfigurationTest.php b/src/TwigHooks/tests/Integration/DependencyInjection/ConfigurationTest.php index 823fbd0e..b5ea3825 100644 --- a/src/TwigHooks/tests/Integration/DependencyInjection/ConfigurationTest.php +++ b/src/TwigHooks/tests/Integration/DependencyInjection/ConfigurationTest.php @@ -67,6 +67,7 @@ public function testItSetsDefaultValuesForHookable(): void 'enabled' => true, 'component' => null, 'props' => [], + 'condition' => null, ], ], ], @@ -122,6 +123,7 @@ public function testItAllowsToUseComponentShortcut(): void 'component' => 'MyAwesomeComponent', 'template' => null, 'props' => [], + 'condition' => null, ], ], ], @@ -156,6 +158,7 @@ public function testItAllowsToUseTemplateShortcut(): void 'component' => null, 'template' => 'some_target.html.twig', 'props' => [], + 'condition' => null, ], ], ], @@ -202,6 +205,61 @@ public function testItThrowsExceptionWhenPropsAreDefinedForNonComponentHookable( ); } + public function testItAllowsToDefineCondition(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'hooks' => [ + 'some_hook' => [ + 'some_hookable' => [ + 'template' => 'some_target.html.twig', + 'condition' => '@=user != null', + ], + ], + ], + ], + ], + [ + 'hooks' => [ + 'some_hook' => [ + 'some_hookable' => [ + 'type' => 'template', + 'context' => [], + 'configuration' => [], + 'priority' => null, + 'enabled' => true, + 'component' => null, + 'template' => 'some_target.html.twig', + 'props' => [], + 'condition' => '@=user != null', + ], + ], + ], + ], + 'hooks.*', + ); + } + + public function testItThrowsExceptionWhenConditionDoesNotStartWithExpressionPrefix(): void + { + $this->assertConfigurationIsInvalid( + [ + [ + 'hooks' => [ + 'some_hook' => [ + 'some_hookable' => [ + 'template' => 'some_target.html.twig', + 'condition' => 'user is not null', + ], + ], + ], + ], + ], + 'must be an expression prefixed with "@=".', + ); + } + protected function getConfiguration(): Configuration { return new Configuration(); diff --git a/src/TwigHooks/tests/Unit/Hook/Renderer/HookRendererTest.php b/src/TwigHooks/tests/Unit/Hook/Renderer/HookRendererTest.php index 6610f702..21ebe5a4 100644 --- a/src/TwigHooks/tests/Unit/Hook/Renderer/HookRendererTest.php +++ b/src/TwigHooks/tests/Unit/Hook/Renderer/HookRendererTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\TestCase; use Sylius\TwigHooks\Hook\Renderer\HookRenderer; use Sylius\TwigHooks\Hookable\AbstractHookable; +use Sylius\TwigHooks\Hookable\Checker\HookableConditionCheckerInterface; use Sylius\TwigHooks\Hookable\Metadata\HookableMetadataFactoryInterface; use Sylius\TwigHooks\Hookable\Renderer\HookableRendererInterface; use Sylius\TwigHooks\Provider\ConfigurationProviderInterface; @@ -41,6 +42,9 @@ final class HookRendererTest extends TestCase /** @var HookableMetadataFactoryInterface&MockObject */ private HookableMetadataFactoryInterface $hookableMetadataFactory; + /** @var HookableConditionCheckerInterface&MockObject */ + private HookableConditionCheckerInterface $hookableConditionChecker; + protected function setUp(): void { $this->hookablesRegistry = $this->createMock(HookablesRegistry::class); @@ -48,6 +52,7 @@ protected function setUp(): void $this->contextProvider = $this->createMock(ContextProviderInterface::class); $this->configurationProvider = $this->createMock(ConfigurationProviderInterface::class); $this->hookableMetadataFactory = $this->createMock(HookableMetadataFactoryInterface::class); + $this->hookableConditionChecker = $this->createMock(HookableConditionCheckerInterface::class); } public function testItReturnsRenderedHookables(): void @@ -58,6 +63,7 @@ public function testItReturnsRenderedHookables(): void $this->hookablesRegistry->method('getEnabledFor')->willReturn([$hookableOne, $hookableTwo]); $this->contextProvider->method('provide')->willReturn([]); $this->configurationProvider->method('provide')->willReturn([]); + $this->hookableConditionChecker->method('isEnabled')->willReturn(true); $this->hookableRenderer->expects($this->exactly(2))->method('render')->willReturnCallback( static fn (AbstractHookable $hookable): string => match ($hookable) { @@ -86,6 +92,26 @@ public function testItReturnsEmptyStringWhenNoHookablesAreFound(): void $this->assertSame('', $result); } + public function testItSkipsHookablesThatAreNotEnabledByTheirCondition(): void + { + $hookableOne = HookableTemplateMotherObject::withName('first_hook'); + $hookableTwo = HookableTemplateMotherObject::withName('second_hook'); + + $this->hookablesRegistry->method('getEnabledFor')->willReturn([$hookableOne, $hookableTwo]); + $this->contextProvider->method('provide')->willReturn([]); + $this->configurationProvider->method('provide')->willReturn([]); + + $this->hookableConditionChecker->expects($this->exactly(2))->method('isEnabled')->willReturnCallback( + static fn (AbstractHookable $hookable): bool => $hookable === $hookableOne, + ); + + $this->hookableRenderer->expects($this->once())->method('render')->willReturn('hookable_one_rendered'); + + $result = $this->getTestSubject()->render(['hook_name']); + + $this->assertSame('hookable_one_rendered', $result); + } + private function getTestSubject(): HookRenderer { return new HookRenderer( @@ -94,6 +120,7 @@ private function getTestSubject(): HookRenderer $this->contextProvider, $this->configurationProvider, $this->hookableMetadataFactory, + $this->hookableConditionChecker, ); } } diff --git a/src/TwigHooks/tests/Unit/Hookable/Checker/HookableConditionCheckerTest.php b/src/TwigHooks/tests/Unit/Hookable/Checker/HookableConditionCheckerTest.php new file mode 100644 index 00000000..2d169af1 --- /dev/null +++ b/src/TwigHooks/tests/Unit/Hookable/Checker/HookableConditionCheckerTest.php @@ -0,0 +1,74 @@ +assertTrue($this->createTestSubject()->isEnabled($hookable, [])); + } + + public function testItReturnsTrueWhenConditionIsSatisfied(): void + { + $hookable = new HookableTemplate('some_hook', 'some_name', 'some_target', condition: '@=user !== null'); + + $this->assertTrue($this->createTestSubject()->isEnabled($hookable, ['user' => new \stdClass()])); + } + + public function testItReturnsFalseWhenConditionIsNotSatisfied(): void + { + $hookable = new HookableTemplate('some_hook', 'some_name', 'some_target', condition: '@=user !== null'); + + $this->assertFalse($this->createTestSubject()->isEnabled($hookable, ['user' => null])); + } + + public function testItResolvesConditionAgainstContextVariable(): void + { + $hookable = new HookableTemplate('some_hook', 'some_name', 'some_target', condition: '@=_context.user !== null'); + + $this->assertTrue($this->createTestSubject()->isEnabled($hookable, ['user' => new \stdClass()])); + } + + public function testItKeepsContextVariableReservedWhenContextContainsCollidingKey(): void + { + $hookable = new HookableTemplate('some_hook', 'some_name', 'some_target', condition: '@=_context.user !== null'); + + $this->assertTrue($this->createTestSubject()->isEnabled($hookable, ['_context' => 'unexpected', 'user' => new \stdClass()])); + } + + public function testItThrowsExceptionWhenConditionCannotBeEvaluated(): void + { + $hookable = new HookableTemplate('some_hook', 'some_name', 'some_target', condition: '@=this is not a valid expression !!!'); + + $this->expectException(InvalidExpressionException::class); + + $this->createTestSubject()->isEnabled($hookable, []); + } + + private function createTestSubject(): HookableConditionCheckerInterface + { + return new HookableConditionChecker(new ExpressionLanguage()); + } +}