Skip to content

Commit

Permalink
Improve doc and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
GromNaN committed Nov 24, 2024
1 parent 1906c8f commit d16812e
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 88 deletions.
44 changes: 14 additions & 30 deletions src/Attribute/AsTwigFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,46 +25,30 @@
* #[AsTwigFilter('foo')]
* function fooFilter(Environment $env, array $context, $string, $arg1 = null, ...) { ... }
*
* {{ 'string'|foo(arg1) }}
*
* @see TwigFilter
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class AsTwigFilter
{
/**
* @param non-empty-string $name The name of the filter in Twig.
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
* @param null|callable(Node):bool $isSafeCallback Function called at compilation time to determine if the filter is safe.
* @param string|null $preEscape Some filters may need to work on input that is already escaped or safe, for
* example when adding (safe) HTML tags to originally unsafe output. In such a
* case, set preEscape to an escape format to escape the input data before it
* is run through the filter.
* @param string[]|null $preservesSafety Preserves the safety of the value that the filter is applied to.
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
*/
public function __construct(
/**
* The name of the filter in Twig.
*
* @var non-empty-string $name
*/
public string $name,

/**
* List of formats in which you want the raw output to be printed unescaped.
*
* @var list<string>|null $isSafe
*/
public ?array $isSafe = null,

/**
* Function called at compilation time to determine if the filter is safe.
*
* @var callable(Node):bool $isSafeCallback
*/
public ?string $isSafeCallback = null,

/**
* Some filters may need to work on input that is already escaped or safe, for
* example when adding (safe) HTML tags to originally unsafe output. In such a
* case, set preEscape to an escape format to escape the input data before it
* is run through the filter.
*/
public mixed $isSafeCallback = null,
public ?string $preEscape = null,

/**
* Preserves the safety of the value that the filter is applied to.
*/
public ?array $preservesSafety = null,

public ?DeprecatedCallableInfo $deprecationInfo = null,
) {
}
Expand Down
32 changes: 11 additions & 21 deletions src/Attribute/AsTwigFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,26 @@
* Additional arguments of the method come from the function call.
*
* #[AsTwigFunction('foo')]
* function fooFunction(Environment $env, array $context, $string, $arg1 = null, ...) { ... }
* function fooFunction(Environment $env, array $context, string $string, $arg1 = null, ...) { ... }
*
* {{ foo('string', arg1) }}
*
* @see TwigFunction
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class AsTwigFunction
{
/**
* @param non-empty-string $name The name of the function in Twig.
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
* @param null|callable(Node):bool $isSafeCallback Function called at compilation time to determine if the function is safe.
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
*/
public function __construct(
/**
* The name of the function in Twig.
*
* @var non-empty-string $name
*/
public string $name,

/**
* List of formats in which you want the raw output to be printed unescaped.
*
* @var list<string>|null $isSafe
*/
public ?array $isSafe = null,

/**
* Function called at compilation time to determine if the function is safe.
*
* @var callable(Node):bool $isSafeCallback
*/
public ?string $isSafeCallback = null,

public ?DeprecatedCallableInfo $deprecationInfo = null,
public mixed $isSafeCallback = null,
public ?DeprecatedCallableInfo $deprecationInfo = null,
) {
}
}
19 changes: 9 additions & 10 deletions src/Attribute/AsTwigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,25 @@
/**
* Registers a method as template test.
*
* If the first argument of the method has Twig\Environment type-hint, the test will receive the current environment.
* If the next argument of the method is named $context and has array type-hint, the test will receive the current context.
* The last argument of the method is the value to be tested, if any.
* The first argument is the value to test and the other arguments are the
* arguments passed to the test in the template.
*
* #[AsTwigTest('foo')]
* public function fooTest(Environment $env, array $context, $value, $arg1 = null) { ... }
* public function fooTest($value, $arg1 = null) { ... }
*
* {% if value is foo(arg1) %}
*
* @see TwigTest
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class AsTwigTest
{
/**
* @param non-empty-string $name The name of the test in Twig.
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
*/
public function __construct(
/**
* The name of the filter in Twig.
*
* @var non-empty-string $name
*/
public string $name,

public ?DeprecatedCallableInfo $deprecationInfo = null,
) {
}
Expand Down
41 changes: 26 additions & 15 deletions src/Extension/AttributeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ final class AttributeExtension extends AbstractExtension
* A list of objects or class names defining filters, functions, and tests using PHP attributes.
* When passing a class name, it must be available in runtimes.
*
* @param class-string[]
* @param list<object|class-string> $classes
*/
public function __construct(array $classes)
{
Expand Down Expand Up @@ -77,15 +77,10 @@ private function initFromAttributes()
$filters = $functions = $tests = [];

foreach ($this->classes as $objectOrClass) {
try {
$reflectionClass = new \ReflectionClass($objectOrClass);
} catch (\ReflectionException $e) {
throw new \LogicException(sprintf('"%s" class requires a list of objects or class name, "%s" given.', __CLASS__, get_debug_type($objectOrClass)), 0, $e);
}

$reflectionClass = new \ReflectionClass($objectOrClass);
$attributes = $reflectionClass->getAttributes(AsTwigExtension::class);
if (!$attributes) {
throw new \LogicException(sprintf('Extension class "%s" must have the attribute "%s" in order to use attributes', is_string($objectOrClass) ? $objectOrClass : get_debug_type($objectOrClass), AsTwigExtension::class));
throw new \LogicException(sprintf('Extension class "%s" must have the attribute "#[%s]" in order to use attributes.', $reflectionClass->getName(), AsTwigExtension::class));
}

foreach ($reflectionClass->getMethods() as $method) {
Expand All @@ -96,11 +91,19 @@ private function initFromAttributes()

$name = $attribute->name;
$parameters = $method->getParameters();
$needsEnvironment = isset($parameters[0]) && Environment::class === $parameters[0]->getType()?->getName();
$needsEnvironment = isset($parameters[0])
&& $parameters[0]->getType() instanceof \ReflectionNamedType
&& Environment::class === $parameters[0]->getType()->getName();
$firstParam = $needsEnvironment ? 1 : 0;
$needsContext = isset($parameters[$firstParam]) && 'context' === $parameters[$firstParam]->getName() && 'array' === $parameters[$firstParam]->getType()?->getName();
$needsContext = isset($parameters[$firstParam])
&& 'context' === $parameters[$firstParam]->getName()
&& $parameters[$firstParam]->getType() instanceof \ReflectionNamedType
&& 'array' === $parameters[$firstParam]->getType()->getName();
$firstParam += $needsContext ? 1 : 0;
$isVariadic = isset($parameters[$firstParam]) && end($parameters)->isVariadic();
if (!isset($parameters[$firstParam])) {
throw new \LogicException(sprintf('The method "%s::%s()" class must have at least one argument for the value to filter.', $reflectionClass->getName(), $method->getName()));
}
$isVariadic = end($parameters)->isVariadic();

$filters[$name] = new TwigFilter($name, [$objectOrClass, $method->getName()], [
'needs_environment' => $needsEnvironment,
Expand All @@ -121,11 +124,16 @@ private function initFromAttributes()

$name = $attribute->name;
$parameters = $method->getParameters();
$needsEnvironment = isset($parameters[0]) && Environment::class === $parameters[0]->getType()?->getName();
$needsEnvironment = isset($parameters[0])
&& $parameters[0]->getType() instanceof \ReflectionNamedType
&& Environment::class === $parameters[0]->getType()->getName();
$firstParam = $needsEnvironment ? 1 : 0;
$needsContext = isset($parameters[$firstParam]) && 'context' === $parameters[$firstParam]->getName() && 'array' === $parameters[$firstParam]->getType()?->getName();
$needsContext = isset($parameters[$firstParam])
&& $parameters[$firstParam]->getType() instanceof \ReflectionNamedType
&& 'array' === $parameters[$firstParam]->getType()->getName();
$firstParam += $needsContext ? 1 : 0;
$isVariadic = isset($parameters[$firstParam]) && end($parameters)->isVariadic();
$isVariadic = isset($parameters[$firstParam])
&& end($parameters)->isVariadic();

$functions[$name] = new TwigFunction($name, [$objectOrClass, $method->getName()], [
'needs_environment' => $needsEnvironment,
Expand All @@ -144,7 +152,10 @@ private function initFromAttributes()

$name = $attribute->name;
$parameters = $method->getParameters();
$isVariadic = isset($parameters[$firstParam]) && end($parameters)->isVariadic();
if (count($parameters) < 1) {
throw new \LogicException(sprintf('The method "%s::%s()" class must have at least one argument for the value to test.', $reflectionClass->getName(), $method->getName()));
}
$isVariadic = end($parameters)->isVariadic();

$tests[$name] = new TwigTest($name, [$objectOrClass, $method->getName()], [
'is_variadic' => $isVariadic,
Expand Down
35 changes: 34 additions & 1 deletion tests/Extension/AttributeExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use PHPUnit\Framework\TestCase;
use Twig\DeprecatedCallableInfo;
use Twig\Extension\AttributeExtension;
use Twig\Tests\Extension\Fixtures\FilterWithoutValue;
use Twig\Tests\Extension\Fixtures\TestWithoutValue;
use Twig\Tests\Extension\Fixtures\ExtensionWithAttributes;
use Twig\TwigFilter;
use Twig\TwigFunction;
Expand Down Expand Up @@ -39,8 +41,8 @@ public static function provideFilters()
yield 'with name' => ['foo', 'fooFilter', ['is_safe' => ['html']]];
yield 'with env' => ['with_env_filter', 'withEnvFilter', ['needs_environment' => true]];
yield 'with context' => ['with_context_filter', 'withContextFilter', ['needs_context' => true]];
yield 'no context' => ['no_context_filter', 'noContextFilter', []];
yield 'with env and context' => ['with_env_and_context_filter', 'withEnvAndContextFilter', ['needs_environment' => true, 'needs_context' => true]];
yield 'no argument' => ['no_arg_filter', 'noArgFilter', []];
yield 'variadic' => ['variadic_filter', 'variadicFilter', ['is_variadic' => true]];
yield 'deprecated' => ['deprecated_filter', 'deprecatedFilter', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]];
yield 'pattern' => ['pattern_*_filter', 'patternFilter', []];
Expand Down Expand Up @@ -69,6 +71,7 @@ public static function provideFunctions()
yield 'with name' => ['foo', 'fooFunction', ['is_safe' => ['html']]];
yield 'with env' => ['with_env_function', 'withEnvFunction', ['needs_environment' => true]];
yield 'with context' => ['with_context_function', 'withContextFunction', ['needs_context' => true]];
yield 'no context' => ['no_context_function', 'noContextFunction', []];
yield 'with env and context' => ['with_env_and_context_function', 'withEnvAndContextFunction', ['needs_environment' => true, 'needs_context' => true]];
yield 'no argument' => ['no_arg_function', 'noArgFunction', []];
yield 'variadic' => ['variadic_function', 'variadicFunction', ['is_variadic' => true]];
Expand Down Expand Up @@ -109,4 +112,34 @@ public function testRuntimeExtension()
$this->assertSame([$class, 'fooFunction'], $extension->getFunctions()[0]->getCallable());
$this->assertSame([$class, 'fooTest'], $extension->getTests()[0]->getCallable());
}

public function testTwigExtensionAttributeIsRequired()
{
$extension = new AttributeExtension([self::class]);

$this->expectException(\LogicException::class);
$this->expectExceptionMessage(sprintf('Extension class "%s" must have the attribute "#[Twig\Attribute\AsTwigExtension]" in order to use attributes', self::class));

$extension->getFilters();
}

public function testFilterRequireOneArgument()
{
$extension = new AttributeExtension([FilterWithoutValue::class]);

$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The method "'.FilterWithoutValue::class.'::myFilter()" class must have at least one argument for the value to filter');

$extension->getTests();
}

public function testTestRequireOneArgument()
{
$extension = new AttributeExtension([TestWithoutValue::class]);

$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The method "'.TestWithoutValue::class.'::myTest()" class must have at least one argument for the value to test');

$extension->getTests();
}
}
27 changes: 16 additions & 11 deletions tests/Extension/Fixtures/ExtensionWithAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
class ExtensionWithAttributes
{
#[AsTwigFilter(name: 'foo', isSafe: ['html'])]
public function fooFilter(string $string)
public function fooFilter(string|int $string)
{
}

Expand All @@ -22,18 +22,18 @@ public function withContextFilter(array $context, string $string)
{
}

#[AsTwigFilter('with_env_filter')]
public function withEnvFilter(Environment $env, string $string)
#[AsTwigFilter('no_context_filter')]
public function noContextFilter($context)
{
}

#[AsTwigFilter('with_env_and_context_filter')]
public function withEnvAndContextFilter(Environment $env, array $context, string $string)
#[AsTwigFilter('with_env_filter')]
public function withEnvFilter(Environment $env, string $string)
{
}

#[AsTwigFilter('no_arg_filter')]
public function noArgFilter()
#[AsTwigFilter('with_env_and_context_filter')]
public function withEnvAndContextFilter(Environment $env, array $context, array $data)
{
}

Expand All @@ -53,7 +53,7 @@ public function patternFilter(string $string)
}

#[AsTwigFunction(name: 'foo', isSafe: ['html'])]
public function fooFunction(string $string)
public function fooFunction(string|int $string)
{
}

Expand All @@ -62,6 +62,11 @@ public function withContextFunction(array $context, string $string)
{
}

#[AsTwigFunction('no_context_function')]
public function noContextFunction($context)
{
}

#[AsTwigFunction('with_env_function')]
public function withEnvFunction(Environment $env, string $string)
{
Expand All @@ -88,17 +93,17 @@ public function deprecatedFunction(string $string)
}

#[AsTwigTest(name: 'foo')]
public function fooTest(string $string)
public function fooTest(string|int $value)
{
}

#[AsTwigTest('variadic_test')]
public function variadicTest(string ...$strings)
public function variadicTest(string ...$value)
{
}

#[AsTwigTest('deprecated_test', deprecationInfo: new DeprecatedCallableInfo('foo/bar', '1.2'))]
public function deprecatedTest(string $string)
public function deprecatedTest($value, $argument)
{
}
}
15 changes: 15 additions & 0 deletions tests/Extension/Fixtures/FilterWithoutValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Twig\Tests\Extension\Fixtures;

use Twig\Attribute\AsTwigExtension;
use Twig\Attribute\AsTwigFilter;

#[AsTwigExtension]
class FilterWithoutValue
{
#[AsTwigFilter('my_filter')]
public function myFilter()
{
}
}
15 changes: 15 additions & 0 deletions tests/Extension/Fixtures/TestWithoutValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Twig\Tests\Extension\Fixtures;

use Twig\Attribute\AsTwigExtension;
use Twig\Attribute\AsTwigTest;

#[AsTwigExtension]
class TestWithoutValue
{
#[AsTwigTest('my_test')]
public function myTest()
{
}
}

0 comments on commit d16812e

Please sign in to comment.