Skip to content

Commit e727c24

Browse files
committed
Allow static methods in dynamic values
1 parent f514388 commit e727c24

18 files changed

Lines changed: 214 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- `ExpressionValues` to enable resolution of route parameter values via expressions by @HypeMC
1313
in https://github.com/sofascore/purgatory-bundle/pull/112
14+
- Ability to pass a static method callable as a `DynamicValues` provider by @HypeMC
15+
in https://github.com/sofascore/purgatory-bundle/pull/137
1416

1517
### Changed
1618

1719
- Method `AbstractValues::toArray()` is now `final` by @Brajk19
1820
in https://github.com/sofascore/purgatory-bundle/pull/130
1921
- Method `AbstractValues::getValues()` is now `protected` by @Brajk19
2022
in https://github.com/sofascore/purgatory-bundle/pull/130
23+
- Rename first constructor argument in `DynamicValues` to `$provider` by @HypeMC
24+
in https://github.com/sofascore/purgatory-bundle/pull/137
2125
- Rename second constructor argument in `DynamicValues` to `$propertyPath` by @Brajk19
2226
in https://github.com/sofascore/purgatory-bundle/pull/130
2327

docs/complex-route-params.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,10 @@ public function listAction(Author $author)
140140
You can also add [custom Expression Language functions](custom-expression-language-functions.md) to extend the available
141141
expression syntax.
142142

143-
### Using Values Provided by a Service
143+
### Using Values Provided by a Service or Static Method
144144

145-
As an alternative to expressions, route parameter values can be provided dynamically by a service. This is particularly
146-
useful when you need route parameters that depend on context or runtime information:
145+
As an alternative to expressions, route parameter values can be provided dynamically by a service or a static method.
146+
This is particularly useful when you need route parameters that depend on context or runtime information:
147147

148148
```php
149149
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\DynamicValues;
@@ -155,12 +155,11 @@ public function listAction()
155155
}
156156
```
157157

158-
By default, the entire entity being purged is passed to the route parameter service.
159-
If your service only needs a specific part of the entity, you can limit what is passed by providing a second argument to
160-
`DynamicValues`.
158+
By default, the entire entity being purged is passed to the route parameter provider. If your provider only needs a
159+
specific part of the entity, you can limit what is passed by providing a second argument to `DynamicValues`.
161160

162161
This argument is a **Symfony PropertyAccess property path** and will be resolved against the entity before being passed
163-
to the service:
162+
to the provider:
164163

165164
```php
166165
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\DynamicValues;
@@ -172,7 +171,7 @@ public function listAction()
172171
}
173172
```
174173

175-
To make the service available for resolving route parameter values, ensure it is tagged correctly in the service
174+
To make a service available for resolving route parameter values, ensure it is tagged correctly in the service
176175
configuration:
177176

178177
```yaml
@@ -197,4 +196,24 @@ class MyService
197196
}
198197
```
199198

199+
You can also reference a static method directly instead of a service:
200+
201+
```php
202+
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\DynamicValues;
203+
204+
#[Route('/posts/{type}', name: 'posts_list', methods: 'GET')]
205+
#[PurgeOn(Post::class, routeParams: ['type' => new DynamicValues([MyClass::class, 'getValue'])])]
206+
public function listAction()
207+
{
208+
}
209+
210+
final class MyClass
211+
{
212+
public static function getValue(Post $post)
213+
{
214+
// Return the desired value for the route parameter
215+
}
216+
}
217+
```
218+
200219
[0]: https://github.com/sofascore/purgatory-bundle/blob/2.x/src/Attribute/AsRouteParamService.php

docs/purge-subscriptions-using-yaml.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,16 @@ posts_list:
7575
class: App\Entity\Post
7676
route_params:
7777
type: !dynamic [ my_service, prop ]
78+
79+
# Using values provided by a static method
80+
posts_list:
81+
class: App\Entity\Post
82+
route_params:
83+
type: !dynamic 'App\\MyClass::getValue'
84+
85+
# Using values provided by a static method with a property path
86+
posts_list:
87+
class: App\Entity\Post
88+
route_params:
89+
type: !dynamic [ 'App\\MyClass::getValue', prop ]
7890
```

phpstan-baseline.neon

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
parameters:
22
ignoreErrors:
3+
-
4+
message: '#^Method Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\DynamicValues\:\:normalizeProvider\(\) should return \(array\<string\>&callable\(\)\: mixed\)\|string but returns non\-empty\-array\<object\|string\>&callable\(\)\: mixed\.$#'
5+
identifier: return.type
6+
count: 1
7+
path: src/Attribute/RouteParamValue/DynamicValues.php
8+
39
-
410
message: '#^Call to function is_a\(\) with arguments class\-string\<BackedEnum\>, ''BackedEnum'' and true will always evaluate to true\.$#'
511
identifier: function.alreadyNarrowedType
@@ -25,31 +31,31 @@ parameters:
2531
path: src/Cache/PropertyResolver/AssociationResolver.php
2632

2733
-
28-
message: '#^Parameter \#1 \$alias of class Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\DynamicValues constructor expects string, bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue given\.$#'
34+
message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue\)\: mixed\)\|null, Closure\(non\-empty\-list\<string\>\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue\)\: \(non\-empty\-list\<string\>\|Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\ValuesInterface\|string\) given\.$#'
2935
identifier: argument.type
3036
count: 1
3137
path: src/Cache/RouteMetadata/YamlMetadataProvider.php
3238

3339
-
34-
message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue\)\: mixed\)\|null, Closure\(non\-empty\-list\<string\>\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue\)\: \(non\-empty\-list\<string\>\|Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\ValuesInterface\|string\) given\.$#'
40+
message: '#^Parameter \#1 \$enum of class Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\EnumValues constructor expects class\-string\<BackedEnum\>, bool\|float\|int\|list\<bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue\>\|string given\.$#'
3541
identifier: argument.type
3642
count: 1
3743
path: src/Cache/RouteMetadata/YamlMetadataProvider.php
3844

3945
-
40-
message: '#^Parameter \#1 \$enum of class Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\EnumValues constructor expects class\-string\<BackedEnum\>, bool\|float\|int\|list\<bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue\>\|string given\.$#'
46+
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\.$#'
4147
identifier: argument.type
4248
count: 1
4349
path: src/Cache/RouteMetadata/YamlMetadataProvider.php
4450

4551
-
46-
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\.$#'
52+
message: '#^Parameter \#1 \$property of class Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\PropertyValues constructor expects string, bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue given\.$#'
4753
identifier: argument.type
4854
count: 1
4955
path: src/Cache/RouteMetadata/YamlMetadataProvider.php
5056

5157
-
52-
message: '#^Parameter \#1 \$property of class Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\PropertyValues constructor expects string, bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue given\.$#'
58+
message: '#^Parameter \#1 \$provider of class Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\DynamicValues constructor expects \(array\<string\>&callable\(\)\: mixed\)\|string, bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue given\.$#'
5359
identifier: argument.type
5460
count: 1
5561
path: src/Cache/RouteMetadata/YamlMetadataProvider.php

src/Attribute/RouteParamValue/DynamicValues.php

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,55 @@
77
final class DynamicValues extends AbstractValues
88
{
99
/**
10-
* @param string $alias Alias defined in {@see AsRouteParamService} attribute
10+
* @var string|callable-array<string>
11+
*/
12+
public readonly string|array $provider;
13+
14+
/**
15+
* @param string|callable-array<string> $provider Alias defined in {@see AsRouteParamService} attribute or static method callable
1116
*/
1217
public function __construct(
13-
public readonly string $alias,
18+
string|array $provider,
1419
public readonly ?string $propertyPath = null,
1520
) {
21+
$this->provider = self::normalizeProvider($provider);
1622
}
1723

1824
/**
19-
* @return non-empty-list<?string>
25+
* @return array<string|callable-array<string>|null>
2026
*/
2127
protected function getValues(): array
2228
{
23-
return [$this->alias, $this->propertyPath];
29+
return [$this->provider, $this->propertyPath];
2430
}
2531

2632
public static function type(): string
2733
{
2834
return 'dynamic';
2935
}
36+
37+
/**
38+
* @param string|callable-array<string|object> $provider
39+
*
40+
* @return string|callable-array<string>
41+
*/
42+
private static function normalizeProvider(string|array $provider): string|array
43+
{
44+
if (\is_string($provider)) {
45+
if (!str_contains($provider, '::')) {
46+
return $provider;
47+
}
48+
$provider = explode('::', $provider);
49+
}
50+
51+
if (!\is_callable($provider)) {
52+
throw new \ValueError('Only static method callables are supported.');
53+
}
54+
55+
if (!\is_string($provider[0])) {
56+
throw new \ValueError('Object callables are not supported.');
57+
}
58+
59+
return $provider;
60+
}
3061
}

src/Cache/PropertyResolver/InverseValuesBuilder/DynamicInverseValuesBuilder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static function for(): string
2020
public function build(ValuesInterface $values, string $associationClass, string $associationTarget): ValuesInterface
2121
{
2222
return new DynamicValues(
23-
alias: $values->alias,
23+
provider: $values->provider,
2424
propertyPath: null !== $values->propertyPath ? \sprintf('%s?.%s', $associationTarget, $values->propertyPath) : $associationTarget,
2525
);
2626
}

src/RouteParamValueResolver/DynamicValuesResolver.php

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
1212

1313
/**
14-
* @implements ValuesResolverInterface<array{0: string, 1: ?string}>
14+
* @implements ValuesResolverInterface<array{0: string|callable-array<string>, 1: ?string}>
1515
*/
1616
final class DynamicValuesResolver implements ValuesResolverInterface
1717
{
@@ -34,23 +34,27 @@ public static function for(): string
3434
*/
3535
public function resolve(array $unresolvedValues, object $entity): array
3636
{
37-
[$alias, $propertyPath] = $unresolvedValues;
38-
39-
try {
40-
/** @var \Closure $routeParamService */
41-
$routeParamService = $this->routeParamServiceLocator->get($alias);
42-
} catch (ServiceNotFoundException $e) {
43-
throw new RuntimeException(\sprintf(
44-
'A route parameter resolver service with the alias "%s" was not found. Did you forget to use the #[AsPurgatoryResolver] attribute on your service?',
45-
$alias,
46-
), previous: $e);
37+
[$provider, $propertyPath] = $unresolvedValues;
38+
39+
if (\is_array($provider)) {
40+
$routeParamProvider = $provider(...);
41+
} else {
42+
try {
43+
/** @var \Closure $routeParamProvider */
44+
$routeParamProvider = $this->routeParamServiceLocator->get($provider);
45+
} catch (ServiceNotFoundException $e) {
46+
throw new RuntimeException(\sprintf(
47+
'A route parameter resolver service with the alias "%s" was not found. Did you forget to use the #[AsPurgatoryResolver] attribute on your service?',
48+
$provider,
49+
), previous: $e);
50+
}
4751
}
4852

4953
/** @var object|scalar|array<object|scalar> $arg */
5054
$arg = null === $propertyPath ? $entity : $this->propertyAccessor->getValue($entity, $propertyPath);
5155

5256
/** @var scalar|list<?scalar>|null $values */
53-
$values = $routeParamService($arg);
57+
$values = $routeParamProvider($arg);
5458

5559
return \is_array($values) ? $values : [$values];
5660
}

tests/Application/ApplicationTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,8 @@ public function testDynamicValues(): void
479479
self::assertUrlIsPurged('/animal/for-rating/106'); // __invoke
480480
self::assertUrlIsPurged('/animal/for-rating/126'); // __invoke
481481
self::assertUrlIsPurged('/animal/for-rating/32'); // getOwnerRating
482+
self::assertUrlIsPurged('/animal/for-rating/600'); // getOtherRating
483+
self::assertUrlIsPurged('/animal/for-rating/375'); // getOtherRating
482484

483485
self::clearPurger();
484486

tests/Application/ConfigurationTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Sofascore\PurgatoryBundle\Tests\Functional\TestApplication\Entity\Person;
2424
use Sofascore\PurgatoryBundle\Tests\Functional\TestApplication\Entity\Vehicle;
2525
use Sofascore\PurgatoryBundle\Tests\Functional\TestApplication\Enum\Country;
26+
use Sofascore\PurgatoryBundle\Tests\Functional\TestApplication\Service\AnimalRatingCalculator;
2627

2728
final class ConfigurationTest extends AbstractKernelTestCase
2829
{
@@ -393,6 +394,10 @@ public static function configurationWithTargetProvider(): iterable
393394
'type' => DynamicValues::type(),
394395
'values' => ['purgatory.animal_rating3', 'owner'],
395396
],
397+
[
398+
'type' => DynamicValues::type(),
399+
'values' => [[AnimalRatingCalculator::class, 'getOtherRating'], null],
400+
],
396401
],
397402
],
398403
],
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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\DynamicValues;
11+
12+
#[CoversClass(DynamicValues::class)]
13+
final class DynamicValuesTest extends TestCase
14+
{
15+
#[TestWith(['alias', 'alias'])]
16+
#[TestWith([[DummyStaticCallable::class, 'handle'], [DummyStaticCallable::class, 'handle']])]
17+
#[TestWith([DummyStaticCallable::class.'::handle', [DummyStaticCallable::class, 'handle']])]
18+
public function testValueNormalization(string|array $input, string|array $expected): void
19+
{
20+
self::assertSame($expected, (new DynamicValues($input))->provider);
21+
}
22+
23+
public function testNonStaticStringCallableIsRejected(): void
24+
{
25+
$this->expectException(\ValueError::class);
26+
$this->expectExceptionMessage('Only static method callables are supported.');
27+
28+
new DynamicValues(DummyInstanceCallable::class.'::handle');
29+
}
30+
31+
public function testObjectCallableIsRejected(): void
32+
{
33+
$this->expectException(\ValueError::class);
34+
$this->expectExceptionMessage('Object callables are not supported.');
35+
36+
new DynamicValues([new DummyInstanceCallable(), 'handle']);
37+
}
38+
}
39+
40+
final class DummyStaticCallable
41+
{
42+
public static function handle(): void
43+
{
44+
}
45+
}
46+
47+
final class DummyInstanceCallable
48+
{
49+
public function handle(): void
50+
{
51+
}
52+
}

0 commit comments

Comments
 (0)