diff --git a/phpstan.neon b/phpstan.neon index daeb8b6..eda747a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -11,4 +11,6 @@ parameters: - '#Call to an undefined method Silex\\Application::render\(\).#' - '#Comparison operation "<" between [1-9] and 3 is always (true|false).#' - '#Else branch is unreachable because previous condition is always true#' + - '#Call to function method_exists\(\) with .Twig\\\\Extension.* and .* will always evaluate to false.#' + - '#Call to an undefined static method Twig\\Extension\\CoreExtension\:\:.+\(\).#' reportUnmatchedIgnoredErrors: false diff --git a/src/Spryker/Shared/Twig/Extender/FilterExtender.php b/src/Spryker/Shared/Twig/Extender/FilterExtender.php index e9afd50..109f43d 100644 --- a/src/Spryker/Shared/Twig/Extender/FilterExtender.php +++ b/src/Spryker/Shared/Twig/Extender/FilterExtender.php @@ -7,22 +7,18 @@ namespace Spryker\Shared\Twig\Extender; +use Spryker\Shared\Twig\Extension\EnvironmentCoreExtensionInterface; use Spryker\Shared\Twig\Filter\FilterFactoryInterface; use Twig\Environment; class FilterExtender implements FilterExtenderInterface { - /** - * @var \Spryker\Shared\Twig\Filter\FilterFactoryInterface - */ - protected FilterFactoryInterface $filterFactory; - /** * @param \Spryker\Shared\Twig\Filter\FilterFactoryInterface $filterFactory + * @param \Spryker\Shared\Twig\Extension\EnvironmentCoreExtensionInterface $environmentCoreExtension */ - public function __construct(FilterFactoryInterface $filterFactory) + public function __construct(protected FilterFactoryInterface $filterFactory, protected EnvironmentCoreExtensionInterface $environmentCoreExtension) { - $this->filterFactory = $filterFactory; } /** @@ -32,6 +28,7 @@ public function __construct(FilterFactoryInterface $filterFactory) */ public function extend(Environment $twig): Environment { + $this->environmentCoreExtension->extend($twig); $twig->addFilter($this->filterFactory->createExecuteFilterIfExistsFilter($twig)); return $twig; diff --git a/src/Spryker/Shared/Twig/Extension/EnvironmentCoreExtension.php b/src/Spryker/Shared/Twig/Extension/EnvironmentCoreExtension.php new file mode 100644 index 0000000..42b532e --- /dev/null +++ b/src/Spryker/Shared/Twig/Extension/EnvironmentCoreExtension.php @@ -0,0 +1,174 @@ +getFilters() as $filter) { + $twig->addFilter($filter); + } + + return $twig; + } + + /** + * @param \Twig\Environment $env + * @param array $array + * @param \Closure $arrow + * + * @return \CallbackFilterIterator|array + */ + public function filter(Environment $env, $array, $arrow) + { + if ($this->isDisallowedPhpFunction($arrow)) { + return $array; + } + + if (method_exists(CoreExtension::class, 'filter')) { + return CoreExtension::filter($env, $array, $arrow); + } + + return twig_array_filter($env, $array, $arrow); + } + + /** + * @param \Twig\Environment $env + * @param array $array + * @param \Closure $arrow + * + * @return array + */ + public function find(Environment $env, $array, $arrow) + { + if ($this->isDisallowedPhpFunction($arrow)) { + return $array; + } + + return CoreExtension::find($env, $array, $arrow); + } + + /** + * @param \Twig\Environment $env + * @param array $array + * @param \Closure $arrow + * + * @return array + */ + public function map(Environment $env, $array, $arrow) + { + if ($this->isDisallowedPhpFunction($arrow)) { + return $array; + } + + if (method_exists(CoreExtension::class, 'map')) { + return CoreExtension::map($env, $array, $arrow); + } + + return twig_array_map($env, $array, $arrow); + } + + /** + * @param \Twig\Environment $env + * @param array $array + * @param \Closure $arrow + * @param mixed|null $initial + * + * @return mixed|null + */ + public function reduce(Environment $env, $array, $arrow, $initial = null) + { + if ($this->isDisallowedPhpFunction($arrow)) { + return $array; + } + + if (method_exists(CoreExtension::class, 'reduce')) { + return CoreExtension::reduce($env, $array, $arrow, $initial); + } + + return twig_array_reduce($env, $array, $arrow, $initial); + } + + /** + * @param \Closure|null $arrow + * + * @return bool + */ + protected function isDisallowedPhpFunction($arrow): bool + { + return in_array($arrow, static::SYSTEM_FUNCTIONS); + } + + /** + * @return array<\Twig\TwigFilter> + */ + protected function getFilters(): array + { + $filters = [ + new TwigFilter('filter', [$this, 'filter'], ['needs_environment' => true]), + new TwigFilter('map', [$this, 'map'], ['needs_environment' => true]), + new TwigFilter('reduce', [$this, 'reduce'], ['needs_environment' => true]), + ]; + + if (method_exists(CoreExtension::class, 'find')) { + $filters[] = new TwigFilter('find', [$this, 'find'], ['needs_environment' => true]); + } + + return $filters; + } +} diff --git a/src/Spryker/Shared/Twig/Extension/EnvironmentCoreExtensionInterface.php b/src/Spryker/Shared/Twig/Extension/EnvironmentCoreExtensionInterface.php new file mode 100644 index 0000000..456b3d1 --- /dev/null +++ b/src/Spryker/Shared/Twig/Extension/EnvironmentCoreExtensionInterface.php @@ -0,0 +1,20 @@ +createFilterFactory(), + $this->createEnvironmentCoreExtension(), ); } @@ -129,6 +132,14 @@ public function createFilterFactory(): FilterFactoryInterface return new FilterFactory(); } + /** + * @return \Spryker\Shared\Twig\Extension\EnvironmentCoreExtensionInterface + */ + public function createEnvironmentCoreExtension(): EnvironmentCoreExtensionInterface + { + return new EnvironmentCoreExtension(); + } + /** * @return array<\Spryker\Shared\TwigExtension\Dependency\Plugin\TwigLoaderPluginInterface> */ diff --git a/src/Spryker/Zed/Twig/Communication/TwigCommunicationFactory.php b/src/Spryker/Zed/Twig/Communication/TwigCommunicationFactory.php index e7a989f..fe1d2d1 100644 --- a/src/Spryker/Zed/Twig/Communication/TwigCommunicationFactory.php +++ b/src/Spryker/Zed/Twig/Communication/TwigCommunicationFactory.php @@ -13,6 +13,8 @@ use Spryker\Shared\Twig\Cache\CacheWriter\FilesystemCacheWriter; use Spryker\Shared\Twig\Extender\FilterExtender; use Spryker\Shared\Twig\Extender\FilterExtenderInterface; +use Spryker\Shared\Twig\Extension\EnvironmentCoreExtension; +use Spryker\Shared\Twig\Extension\EnvironmentCoreExtensionInterface; use Spryker\Shared\Twig\Filter\FilterFactory; use Spryker\Shared\Twig\Filter\FilterFactoryInterface; use Spryker\Shared\Twig\Loader\FilesystemLoader; @@ -151,6 +153,7 @@ public function createFilterExtender(): FilterExtenderInterface { return new FilterExtender( $this->createFilterFactory(), + $this->createEnvironmentCoreExtension(), ); } @@ -161,4 +164,12 @@ public function createFilterFactory(): FilterFactoryInterface { return new FilterFactory(); } + + /** + * @return \Spryker\Shared\Twig\Extension\EnvironmentCoreExtensionInterface + */ + public function createEnvironmentCoreExtension(): EnvironmentCoreExtensionInterface + { + return new EnvironmentCoreExtension(); + } } diff --git a/tests/SprykerTest/Shared/Twig/Extension/EnvironmentCoreExtensionTest.php b/tests/SprykerTest/Shared/Twig/Extension/EnvironmentCoreExtensionTest.php new file mode 100644 index 0000000..b26b1f1 --- /dev/null +++ b/tests/SprykerTest/Shared/Twig/Extension/EnvironmentCoreExtensionTest.php @@ -0,0 +1,50 @@ +tester->createTwigEnvironment(new ArrayLoader([ + 'test' => "{{ ['id'] | map('system') | join }} {{ ['php -v'] | reduce('exec') | join }} {{ [' php '] | map(value => value | trim) | join }}", + ])); + $environmentCoreExtension->extend($twig); + + // Act + $output = $twig->render('test'); + + // Assert + $this->assertSame('id php -v php', $output); + } +} diff --git a/tests/SprykerTest/Shared/Twig/_support/TwigSharedTester.php b/tests/SprykerTest/Shared/Twig/_support/TwigSharedTester.php index 995edda..d98361e 100644 --- a/tests/SprykerTest/Shared/Twig/_support/TwigSharedTester.php +++ b/tests/SprykerTest/Shared/Twig/_support/TwigSharedTester.php @@ -8,6 +8,8 @@ namespace SprykerTest\Shared\Twig; use Codeception\Actor; +use Twig\Environment; +use Twig\Loader\LoaderInterface; /** * @method void wantToTest($text) @@ -26,4 +28,19 @@ class TwigSharedTester extends Actor { use _generated\TwigSharedTesterActions; + + /** + * @var array + */ + public const ENVIRONMENT_FILTERS = ['sort', 'filter', 'map', 'reduce', 'find']; + + /** + * @param \Twig\Loader\LoaderInterface|null $loader + * + * @return \Twig\Environment + */ + public function createTwigEnvironment(?LoaderInterface $loader = null): Environment + { + return new Environment($loader); + } }