Skip to content

Commit f74a3af

Browse files
committed
Add ExpressionValues
1 parent d65edca commit f74a3af

File tree

12 files changed

+280
-20
lines changed

12 files changed

+280
-20
lines changed

config/services.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Sofascore\PurgatoryBundle\RouteParamValueResolver\CompoundValuesResolver;
2929
use Sofascore\PurgatoryBundle\RouteParamValueResolver\DynamicValuesResolver;
3030
use Sofascore\PurgatoryBundle\RouteParamValueResolver\EnumValuesResolver;
31+
use Sofascore\PurgatoryBundle\RouteParamValueResolver\ExpressionValuesResolver;
3132
use Sofascore\PurgatoryBundle\RouteParamValueResolver\PropertyValuesResolver;
3233
use Sofascore\PurgatoryBundle\RouteParamValueResolver\RawValuesResolver;
3334
use Sofascore\PurgatoryBundle\RouteProvider\AbstractEntityRouteProvider;
@@ -224,6 +225,12 @@
224225
service('sofascore.purgatory.property_accessor'),
225226
])
226227

228+
->set('sofascore.purgatory.route_parameter_resolver.expression', ExpressionValuesResolver::class)
229+
->tag('purgatory.route_param_value_resolver')
230+
->args([
231+
service('sofascore.purgatory.expression_language')->nullOnInvalid(),
232+
])
233+
227234
->set('sofascore.purgatory.property_accessor', PurgatoryPropertyAccessor::class)
228235
->args([
229236
service('property_accessor'),
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sofascore\PurgatoryBundle\Attribute\RouteParamValue;
6+
7+
use Sofascore\PurgatoryBundle\Exception\LogicException;
8+
use Symfony\Component\ExpressionLanguage\Expression;
9+
10+
final class ExpressionValues extends AbstractValues
11+
{
12+
private readonly Expression $expression;
13+
14+
public function __construct(
15+
string|Expression $expression,
16+
) {
17+
$this->expression = self::normalizeExpression($expression);
18+
}
19+
20+
/**
21+
* @return list<Expression>
22+
*/
23+
public function getValues(): array
24+
{
25+
return [$this->expression];
26+
}
27+
28+
public static function type(): string
29+
{
30+
return 'expression';
31+
}
32+
33+
private static function normalizeExpression(string|Expression $expression): Expression
34+
{
35+
if ($expression instanceof Expression) {
36+
return $expression;
37+
}
38+
39+
if (!class_exists(Expression::class)) {
40+
throw new LogicException(\sprintf('You cannot use "%s" because the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".', self::class));
41+
}
42+
43+
return new Expression($expression);
44+
}
45+
}

src/Cache/RouteMetadata/YamlMetadataProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\CompoundValues;
99
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\DynamicValues;
1010
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\EnumValues;
11+
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ExpressionValues;
1112
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\PropertyValues;
1213
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\RawValues;
1314
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ValuesInterface;
@@ -182,12 +183,14 @@ private function buildRouteParam(string|array|TaggedValue $routeParam): string|a
182183
CompoundValues::type() => new CompoundValues(...array_map($this->buildRouteParam(...), $value)),
183184
DynamicValues::type() => new DynamicValues(...((array) $value)),
184185
EnumValues::type() => new EnumValues($value),
186+
ExpressionValues::type() => new ExpressionValues($value),
185187
PropertyValues::type() => new PropertyValues(...((array) $value)),
186188
RawValues::type() => new RawValues(...((array) $value)),
187189
default => throw new UnknownYamlTagException($tag, [
188190
CompoundValues::type(),
189191
DynamicValues::type(),
190192
EnumValues::type(),
193+
ExpressionValues::type(),
191194
PropertyValues::type(),
192195
RawValues::type(),
193196
]),

src/Cache/Subscription/PurgeSubscriptionProvider.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Doctrine\Persistence\ManagerRegistry;
88
use Psr\Container\ContainerInterface;
9+
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ExpressionValues;
910
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\PropertyValues;
1011
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ValuesInterface;
1112
use Sofascore\PurgatoryBundle\Attribute\Target\TargetInterface;
@@ -58,7 +59,7 @@ private function provideFromMetadata(RouteMetadataProviderInterface $routeMetada
5859
$purgeOn = $routeMetadata->purgeOn;
5960

6061
if (null !== $purgeOn->if) {
61-
$this->validateIfExpression($purgeOn->if, $routeMetadata->routeName);
62+
$this->validateExpression($purgeOn->if, $routeMetadata->routeName);
6263
}
6364

6465
// if route parameters are not specified, they are same as path variables
@@ -73,6 +74,11 @@ private function provideFromMetadata(RouteMetadataProviderInterface $routeMetada
7374
$routeParams[$pathVariable] = new PropertyValues($pathVariable);
7475
}
7576
} else {
77+
foreach ($purgeOn->routeParams as $values) {
78+
if ($values instanceof ExpressionValues) {
79+
$this->validateExpression($values->getValues()[0], $routeMetadata->routeName);
80+
}
81+
}
7682
$this->validateRouteParams(array_keys($purgeOn->routeParams), $routeMetadata);
7783
$routeParams = $purgeOn->routeParams;
7884
}
@@ -140,7 +146,7 @@ private function validateRouteParams(array $routeParams, RouteMetadata $routeMet
140146
}
141147
}
142148

143-
private function validateIfExpression(Expression $expression, string $routeName): void
149+
private function validateExpression(Expression $expression, string $routeName): void
144150
{
145151
try {
146152
$this->expressionLanguage?->lint($expression, ['obj']);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sofascore\PurgatoryBundle\RouteParamValueResolver;
6+
7+
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ExpressionValues;
8+
use Sofascore\PurgatoryBundle\Exception\LogicException;
9+
use Symfony\Component\ExpressionLanguage\Expression;
10+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
11+
12+
/**
13+
* @implements ValuesResolverInterface<array{0: Expression}>
14+
*/
15+
final class ExpressionValuesResolver implements ValuesResolverInterface
16+
{
17+
public function __construct(
18+
private readonly ?ExpressionLanguage $expressionLanguage,
19+
) {
20+
}
21+
22+
/**
23+
* {@inheritDoc}
24+
*/
25+
public static function for(): string
26+
{
27+
return ExpressionValues::type();
28+
}
29+
30+
/**
31+
* {@inheritDoc}
32+
*/
33+
public function resolve(array $unresolvedValues, object $entity): array
34+
{
35+
$expression = $unresolvedValues[0];
36+
37+
/** @var scalar|list<?scalar>|null $values */
38+
$values = $this->getExpressionLanguage()->evaluate($expression, ['obj' => $entity]);
39+
40+
return \is_array($values) ? $values : [$values];
41+
}
42+
43+
private function getExpressionLanguage(): ExpressionLanguage
44+
{
45+
return $this->expressionLanguage
46+
?? throw new LogicException('You cannot use expressions because the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
47+
}
48+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sofascore\PurgatoryBundle\Tests\Attribute\RouteParamValue;
6+
7+
use PHPUnit\Framework\Attributes\CoversClass;
8+
use PHPUnit\Framework\Attributes\TestWith;
9+
use PHPUnit\Framework\TestCase;
10+
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ExpressionValues;
11+
use Symfony\Component\ExpressionLanguage\Expression;
12+
13+
#[CoversClass(ExpressionValues::class)]
14+
final class ExpressionValuesTest extends TestCase
15+
{
16+
#[TestWith(['constant("App\\\\Foo::MAP")[obj.geValue()]'])]
17+
#[TestWith([new Expression('constant("App\\\\Foo::MAP")[obj.geValue()]')])]
18+
public function testValueNormalization(string|Expression $expression): void
19+
{
20+
$values = (new ExpressionValues($expression))->getValues();
21+
22+
self::assertArrayHasKey(0, $values);
23+
self::assertInstanceOf(Expression::class, $values[0]);
24+
self::assertSame((string) $expression, (string) $values[0]);
25+
}
26+
}

tests/Cache/RouteMetadata/Fixtures/config/purge_on_with_tags.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ foo_bar:
1010
- !enum Sofascore\PurgatoryBundle\Tests\Cache\RouteMetadata\Fixtures\DummyEnum
1111
param5: !dynamic foo
1212
param6: !dynamic [ foo, bar ]
13+
param7: !expression 'constant("App\\Foo::MAP")[obj.geValue()]'

tests/Cache/RouteMetadata/YamlMetadataProviderTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\CompoundValues;
1111
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\DynamicValues;
1212
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\EnumValues;
13+
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ExpressionValues;
1314
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\PropertyValues;
1415
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\RawValues;
1516
use Sofascore\PurgatoryBundle\Attribute\Target\ForGroups;
@@ -120,6 +121,7 @@ public function testRouteMetadataWithTags(): void
120121
),
121122
'param5' => new DynamicValues('foo'),
122123
'param6' => new DynamicValues('foo', 'bar'),
124+
'param7' => new ExpressionValues('constant("App\\\\Foo::MAP")[obj.geValue()]'),
123125
], $metadata[0]->purgeOn->routeParams);
124126
self::assertNull($metadata[0]->purgeOn->if);
125127
self::assertNull($metadata[0]->purgeOn->actions);
@@ -255,7 +257,7 @@ public function testExceptionIsThrownForInvalidRoute(): void
255257
}
256258

257259
#[TestWith(['purge_on_with_unknown_target_tag.yaml', 'Unknown YAML tag "for_unknown" provided, known tags are "for_groups", "for_properties".'])]
258-
#[TestWith(['purge_on_with_unknown_route_param_tag.yaml', 'Unknown YAML tag "unknown" provided, known tags are "compound", "dynamic", "enum", "property", "raw".'])]
260+
#[TestWith(['purge_on_with_unknown_route_param_tag.yaml', 'Unknown YAML tag "unknown" provided, known tags are "compound", "dynamic", "enum", "expression", "property", "raw".'])]
259261
public function testExceptionIsThrownForUnknownTags(string $file, string $message): void
260262
{
261263
$collection = new RouteCollection();

tests/Cache/Subscription/PurgeSubscriptionProviderTest.php

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
use Doctrine\Persistence\ManagerRegistry;
1010
use PHPUnit\Framework\Attributes\CoversClass;
1111
use PHPUnit\Framework\Attributes\DataProvider;
12-
use PHPUnit\Framework\Attributes\TestWith;
1312
use PHPUnit\Framework\TestCase;
1413
use Psr\Container\ContainerInterface;
1514
use Sofascore\PurgatoryBundle\Attribute\PurgeOn;
15+
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ExpressionValues;
1616
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\PropertyValues;
1717
use Sofascore\PurgatoryBundle\Attribute\Target\ForProperties;
1818
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\SubscriptionResolverInterface;
@@ -486,22 +486,49 @@ class: 'FooEntity',
486486
];
487487
}
488488

489-
#[TestWith([
490-
'if' => 'invalidObj.getMethod()',
491-
'expectedMessage' => 'Invalid "if" expression provided for route "foo": "Variable "invalidObj" is not valid around position 1 for expression `invalidObj.getMethod()`."',
492-
])]
493-
#[TestWith([
494-
'if' => 'entity !== null',
495-
'expectedMessage' => 'Invalid "if" expression provided for route "foo": "Variable "entity" is not valid around position 1 for expression `entity !== null`."',
496-
])]
497-
#[TestWith([
498-
'if' => 'some_function(obj)',
499-
'expectedMessage' => 'Invalid "if" expression provided for route "foo": "The function "some_function" does not exist around position 1 for expression `some_function(obj)`."',
500-
])]
501-
#[TestWith([
502-
'if' => 'valid_function(author)',
503-
'expectedMessage' => 'Invalid "if" expression provided for route "foo": "Variable "author" is not valid around position 16 for expression `valid_function(author)`."',
504-
])]
489+
#[DataProvider('provideInvalidExpressions')]
490+
public function testExceptionIsThrownOnInvalidRouteParamsExpression(string $expression, string $expectedMessage): void
491+
{
492+
$routeMetadataProvider = self::createStub(RouteMetadataProviderInterface::class);
493+
$routeMetadataProvider->method('provide')
494+
->willReturnCallback(function () use ($expression): iterable {
495+
yield new RouteMetadata(
496+
routeName: 'foo',
497+
route: new Route('/{foo}'),
498+
purgeOn: new PurgeOn(
499+
class: 'FooEntity',
500+
routeParams: ['foo' => new ExpressionValues($expression)],
501+
),
502+
reflectionMethod: null,
503+
);
504+
});
505+
506+
$purgeSubscriptionProvider = new PurgeSubscriptionProvider(
507+
subscriptionResolvers: [],
508+
routeMetadataProviders: [$routeMetadataProvider],
509+
managerRegistry: self::createStub(ManagerRegistry::class),
510+
targetResolverLocator: self::createStub(ContainerInterface::class),
511+
expressionLanguage: new ExpressionLanguage(
512+
providers: [
513+
new class implements ExpressionFunctionProviderInterface {
514+
public function getFunctions(): array
515+
{
516+
return [
517+
new ExpressionFunction('valid_function', function () {}, function () {}),
518+
];
519+
}
520+
},
521+
],
522+
),
523+
);
524+
525+
$this->expectException(InvalidIfExpressionException::class);
526+
$this->expectExceptionMessage($expectedMessage);
527+
528+
[...$purgeSubscriptionProvider->provide()];
529+
}
530+
531+
#[DataProvider('provideInvalidExpressions')]
505532
public function testExceptionIsThrownOnInvalidIfExpression(string $if, string $expectedMessage): void
506533
{
507534
$routeMetadataProvider = self::createStub(RouteMetadataProviderInterface::class);
@@ -542,4 +569,24 @@ public function getFunctions(): array
542569

543570
[...$purgeSubscriptionProvider->provide()];
544571
}
572+
573+
public static function provideInvalidExpressions(): iterable
574+
{
575+
yield [
576+
'invalidObj.getMethod()',
577+
'Invalid "if" expression provided for route "foo": "Variable "invalidObj" is not valid around position 1 for expression `invalidObj.getMethod()`."',
578+
];
579+
yield [
580+
'entity !== null',
581+
'Invalid "if" expression provided for route "foo": "Variable "entity" is not valid around position 1 for expression `entity !== null`."',
582+
];
583+
yield [
584+
'some_function(obj)',
585+
'Invalid "if" expression provided for route "foo": "The function "some_function" does not exist around position 1 for expression `some_function(obj)`."',
586+
];
587+
yield [
588+
'valid_function(author)',
589+
'Invalid "if" expression provided for route "foo": "Variable "author" is not valid around position 16 for expression `valid_function(author)`."',
590+
];
591+
}
545592
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sofascore\PurgatoryBundle\Tests\RouteParamValueResolver;
6+
7+
use PHPUnit\Framework\Attributes\CoversClass;
8+
use PHPUnit\Framework\Attributes\TestWith;
9+
use PHPUnit\Framework\TestCase;
10+
use Sofascore\PurgatoryBundle\Exception\LogicException;
11+
use Sofascore\PurgatoryBundle\RouteParamValueResolver\ExpressionValuesResolver;
12+
use Sofascore\PurgatoryBundle\Tests\RouteParamValueResolver\Fixtures\Foo;
13+
use Symfony\Component\ExpressionLanguage\Expression;
14+
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
15+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
16+
use Symfony\Component\HttpKernel\Kernel;
17+
18+
#[CoversClass(ExpressionValuesResolver::class)]
19+
final class ExpressionValuesResolverTest extends TestCase
20+
{
21+
#[TestWith(['constant("Sofascore\\\\PurgatoryBundle\\\\Tests\\\\RouteParamValueResolver\\\\Fixtures\\\\Map::NAMES")[obj.name].value', 'one', 1])]
22+
#[TestWith(['enum("Sofascore\\\\PurgatoryBundle\\\\Tests\\\\RouteParamValueResolver\\\\Fixtures\\\\Map::"~obj.name).value', 'Two', 2])]
23+
public function testResolve(string $expression, string $name, int $expectedValue): void
24+
{
25+
$resolver = new ExpressionValuesResolver($expressionLang = new ExpressionLanguage());
26+
27+
if (Kernel::MAJOR_VERSION <= 5) {
28+
$expressionLang->addFunction(new ExpressionFunction('enum',
29+
static fn ($str): string => \sprintf("(\constant(\$v = (%s))) instanceof \UnitEnum ? \constant(\$v) : throw new \TypeError(\sprintf('The string \"%%s\" is not the name of a valid enum case.', \$v))", $str),
30+
static function ($arguments, $str): \UnitEnum {
31+
$value = \constant($str);
32+
33+
if (!$value instanceof \UnitEnum) {
34+
throw new \TypeError(\sprintf('The string "%s" is not the name of a valid enum case.', $str));
35+
}
36+
37+
return $value;
38+
},
39+
));
40+
}
41+
42+
$foo = new Foo();
43+
$foo->name = $name;
44+
45+
self::assertSame([$expectedValue], $resolver->resolve([new Expression($expression)], $foo));
46+
}
47+
48+
public function testExceptionIsThrownWhenExpressionLangIsNotAvailable(): void
49+
{
50+
$resolver = new ExpressionValuesResolver(null);
51+
52+
$this->expectException(LogicException::class);
53+
$this->expectExceptionMessage('You cannot use expressions because the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
54+
55+
$resolver->resolve([new Expression('expr')], new \stdClass());
56+
}
57+
}

0 commit comments

Comments
 (0)