Skip to content

Commit 5cfba3d

Browse files
committed
Add ExpressionValues
1 parent 1ac87a7 commit 5cfba3d

25 files changed

Lines changed: 605 additions & 51 deletions

File tree

config/services.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
use Sofascore\PurgatoryBundle\Cache\Configuration\ConfigurationLoader;
99
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\AssociationResolver;
1010
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\EmbeddableResolver;
11+
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\ExpressionLanguage\InverseRelationExpressionTransformer;
1112
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\InverseValuesBuilder\CompoundInverseValuesBuilder;
1213
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\InverseValuesBuilder\DynamicInverseValuesBuilder;
14+
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\InverseValuesBuilder\ExpressionInverseValuesBuilder;
1315
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\InverseValuesBuilder\PropertyInverseValuesBuilder;
1416
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\MethodResolver;
1517
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\PropertyResolver;
@@ -31,6 +33,7 @@
3133
use Sofascore\PurgatoryBundle\RouteParamValueResolver\CompoundValuesResolver;
3234
use Sofascore\PurgatoryBundle\RouteParamValueResolver\DynamicValuesResolver;
3335
use Sofascore\PurgatoryBundle\RouteParamValueResolver\EnumValuesResolver;
36+
use Sofascore\PurgatoryBundle\RouteParamValueResolver\ExpressionValuesResolver;
3437
use Sofascore\PurgatoryBundle\RouteParamValueResolver\PropertyValuesResolver;
3538
use Sofascore\PurgatoryBundle\RouteParamValueResolver\RawValuesResolver;
3639
use Sofascore\PurgatoryBundle\RouteProvider\AbstractEntityRouteProvider;
@@ -96,8 +99,8 @@
9699
->set('sofascore.purgatory.subscription_resolver.association', AssociationResolver::class)
97100
->tag('purgatory.subscription_resolver')
98101
->args([
99-
service('property_info.reflection_extractor'),
100102
tagged_locator('purgatory.inverse_values_builder', defaultIndexMethod: 'for'),
103+
service('sofascore.purgatory.inverse_relation_expression_transformer'),
101104
])
102105

103106
->set('sofascore.purgatory.subscription_resolver.embeddable', EmbeddableResolver::class)
@@ -106,6 +109,11 @@
106109
service('doctrine'),
107110
])
108111

112+
->set('sofascore.purgatory.inverse_relation_expression_transformer', InverseRelationExpressionTransformer::class)
113+
->args([
114+
service('property_info.reflection_extractor'),
115+
])
116+
109117
->set('sofascore.purgatory.inverse_values_builder.compound', CompoundInverseValuesBuilder::class)
110118
->tag('purgatory.inverse_values_builder')
111119
->args([
@@ -117,6 +125,12 @@
117125
->set('sofascore.purgatory.inverse_values_builder.dynamic', DynamicInverseValuesBuilder::class)
118126
->tag('purgatory.inverse_values_builder')
119127

128+
->set('sofascore.purgatory.inverse_values_builder.expression', ExpressionInverseValuesBuilder::class)
129+
->tag('purgatory.inverse_values_builder')
130+
->args([
131+
service('sofascore.purgatory.inverse_relation_expression_transformer'),
132+
])
133+
120134
->set('sofascore.purgatory.inverse_values_builder.property', PropertyInverseValuesBuilder::class)
121135
->tag('purgatory.inverse_values_builder')
122136

@@ -243,6 +257,12 @@
243257
service('sofascore.purgatory.property_accessor'),
244258
])
245259

260+
->set('sofascore.purgatory.route_parameter_resolver.expression', ExpressionValuesResolver::class)
261+
->tag('purgatory.route_param_value_resolver')
262+
->args([
263+
service('sofascore.purgatory.expression_language')->nullOnInvalid(),
264+
])
265+
246266
->set('sofascore.purgatory.property_accessor', PurgatoryPropertyAccessor::class)
247267
->args([
248268
service('property_accessor'),

phpstan-baseline.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ parameters:
5454
count: 1
5555
path: src/Cache/RouteMetadata/YamlMetadataProvider.php
5656

57+
-
58+
message: '#^Parameter \#1 \$expression of class Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\ExpressionValues constructor expects string\|Symfony\\Component\\ExpressionLanguage\\Expression, bool\|float\|int\|list\<bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue\>\|string given\.$#'
59+
identifier: argument.type
60+
count: 1
61+
path: src/Cache/RouteMetadata/YamlMetadataProvider.php
62+
5763
-
5864
message: '#^Parameter \#1 \$property of class Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\PropertyValues constructor expects string, bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue given\.$#'
5965
identifier: argument.type

psalm-baseline.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
<code><![CDATA[$alias]]></code>
2424
</PossiblyUndefinedArrayOffset>
2525
</file>
26+
<file src="src/Cache/PropertyResolver/InverseValuesBuilder/ExpressionInverseValuesBuilder.php">
27+
<PossiblyUndefinedArrayOffset>
28+
<code><![CDATA[$expression]]></code>
29+
</PossiblyUndefinedArrayOffset>
30+
</file>
2631
<file src="src/Cache/RouteMetadata/AttributeMetadataProvider.php">
2732
<PossiblyUndefinedArrayOffset>
2833
<code><![CDATA[$method]]></code>
@@ -38,6 +43,7 @@
3843
</ArgumentTypeCoercion>
3944
<InvalidArgument>
4045
<code><![CDATA[$value]]></code>
46+
<code><![CDATA[$value]]></code>
4147
</InvalidArgument>
4248
<PossiblyInvalidArgument>
4349
<code><![CDATA[$value]]></code>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 function toArray(): array
29+
{
30+
return [
31+
'type' => self::type(),
32+
'values' => [(string) $this->expression],
33+
];
34+
}
35+
36+
public static function type(): string
37+
{
38+
return 'expression';
39+
}
40+
41+
private static function normalizeExpression(string|Expression $expression): Expression
42+
{
43+
if ($expression instanceof Expression) {
44+
return $expression;
45+
}
46+
47+
if (!class_exists(Expression::class)) {
48+
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));
49+
}
50+
51+
return new Expression($expression);
52+
}
53+
}

src/Cache/PropertyResolver/AssociationResolver.php

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,16 @@
1010
use Doctrine\Persistence\Mapping\ClassMetadata;
1111
use Psr\Container\ContainerInterface;
1212
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ValuesInterface;
13+
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\ExpressionLanguage\InverseRelationExpressionTransformer;
1314
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\InverseValuesBuilder\InverseValuesBuilderInterface;
1415
use Sofascore\PurgatoryBundle\Cache\RouteMetadata\RouteMetadata;
1516
use Sofascore\PurgatoryBundle\Cache\Subscription\PurgeSubscription;
16-
use Sofascore\PurgatoryBundle\Exception\PropertyNotAccessibleException;
17-
use Symfony\Component\ExpressionLanguage\Expression;
18-
use Symfony\Component\PropertyInfo\PropertyReadInfo;
19-
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
2017

2118
final class AssociationResolver implements SubscriptionResolverInterface
2219
{
2320
public function __construct(
24-
private readonly PropertyReadInfoExtractorInterface $extractor,
2521
private readonly ContainerInterface $inverseValuesBuilderLocator,
22+
private readonly InverseRelationExpressionTransformer $expressionTransformer,
2623
) {
2724
}
2825

@@ -76,10 +73,7 @@ public function resolveSubscription(
7673
}
7774

7875
if (null !== $if = $routeMetadata->purgeOn->if) {
79-
$expression = (string) $if;
80-
$getter = $this->createGetter($associationClass, $associationTarget);
81-
$inverseIf = str_replace('obj', 'obj.'.$getter, $expression);
82-
$if = new Expression("obj.$getter !== null && ($inverseIf)");
76+
$if = $this->expressionTransformer->transform($if, $associationClass, $associationTarget, 'false');
8377
}
8478

8579
yield new PurgeSubscription(
@@ -111,19 +105,4 @@ private function getInverseValuesBuilderFor(ValuesInterface $values): ?InverseVa
111105

112106
return $builder;
113107
}
114-
115-
private function createGetter(string $class, string $property): string
116-
{
117-
if (null === $readInfo = $this->extractor->getReadInfo($class, $property)) {
118-
throw new PropertyNotAccessibleException($class, $property);
119-
}
120-
121-
/** @var PropertyReadInfo::TYPE_* $type */
122-
$type = $readInfo->getType();
123-
124-
return match ($type) {
125-
PropertyReadInfo::TYPE_METHOD => $readInfo->getName().'()',
126-
PropertyReadInfo::TYPE_PROPERTY => $readInfo->getName(),
127-
};
128-
}
129108
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sofascore\PurgatoryBundle\Cache\PropertyResolver\ExpressionLanguage;
6+
7+
use Sofascore\PurgatoryBundle\Exception\PropertyNotAccessibleException;
8+
use Symfony\Component\ExpressionLanguage\Expression;
9+
use Symfony\Component\PropertyInfo\PropertyReadInfo;
10+
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
11+
12+
final class InverseRelationExpressionTransformer
13+
{
14+
public function __construct(
15+
private readonly PropertyReadInfoExtractorInterface $extractor,
16+
) {
17+
}
18+
19+
public function transform(Expression $expression, string $class, string $property, string $fallback): Expression
20+
{
21+
$getter = $this->createGetter($class, $property);
22+
$inverseExpression = str_replace('obj', 'obj.'.$getter, (string) $expression);
23+
24+
return new Expression("obj.$getter !== null ? ($inverseExpression) : $fallback");
25+
}
26+
27+
private function createGetter(string $class, string $property): string
28+
{
29+
if (null === $readInfo = $this->extractor->getReadInfo($class, $property)) {
30+
throw new PropertyNotAccessibleException($class, $property);
31+
}
32+
33+
/** @var PropertyReadInfo::TYPE_* $type */
34+
$type = $readInfo->getType();
35+
36+
return match ($type) {
37+
PropertyReadInfo::TYPE_METHOD => $readInfo->getName().'()',
38+
PropertyReadInfo::TYPE_PROPERTY => $readInfo->getName(),
39+
};
40+
}
41+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sofascore\PurgatoryBundle\Cache\PropertyResolver\InverseValuesBuilder;
6+
7+
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ExpressionValues;
8+
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\ValuesInterface;
9+
use Sofascore\PurgatoryBundle\Cache\PropertyResolver\ExpressionLanguage\InverseRelationExpressionTransformer;
10+
11+
/**
12+
* @implements InverseValuesBuilderInterface<ExpressionValues>
13+
*/
14+
final class ExpressionInverseValuesBuilder implements InverseValuesBuilderInterface
15+
{
16+
public function __construct(
17+
private readonly InverseRelationExpressionTransformer $expressionTransformer,
18+
) {
19+
}
20+
21+
public static function for(): string
22+
{
23+
return ExpressionValues::type();
24+
}
25+
26+
public function build(ValuesInterface $values, string $associationClass, string $associationTarget): ValuesInterface
27+
{
28+
[$expression] = $values->getValues();
29+
30+
$inverseExpression = $this->expressionTransformer->transform($expression, $associationClass, $associationTarget, 'null');
31+
32+
return new ExpressionValues($inverseExpression);
33+
}
34+
}

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+
}

0 commit comments

Comments
 (0)