Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

### Changed

- Method `AbstractValues::toArray()` is now `final` by @Brajk19
in https://github.com/sofascore/purgatory-bundle/pull/130
- Method `AbstractValues::getValues()` is now `protected` by @Brajk19
in https://github.com/sofascore/purgatory-bundle/pull/130
- Rename first constructor argument in `DynamicValues` to `$provider` by @HypeMC
in https://github.com/sofascore/purgatory-bundle/pull/137
- Rename second constructor argument in `DynamicValues` to `$propertyPath` by @Brajk19
in https://github.com/sofascore/purgatory-bundle/pull/130

Expand Down
35 changes: 27 additions & 8 deletions docs/complex-route-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@ public function listAction(Author $author)
You can also add [custom Expression Language functions](custom-expression-language-functions.md) to extend the available
expression syntax.

### Using Values Provided by a Service
### Using Values Provided by a Service or Static Method

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

```php
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\DynamicValues;
Expand All @@ -155,12 +155,11 @@ public function listAction()
}
```

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

This argument is a **Symfony PropertyAccess property path** and will be resolved against the entity before being passed
to the service:
to the provider:

```php
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\DynamicValues;
Expand All @@ -172,7 +171,7 @@ public function listAction()
}
```

To make the service available for resolving route parameter values, ensure it is tagged correctly in the service
To make a service available for resolving route parameter values, ensure it is tagged correctly in the service
configuration:

```yaml
Expand All @@ -197,4 +196,24 @@ class MyService
}
```

You can also reference a static method directly instead of a service:

```php
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\DynamicValues;

#[Route('/posts/{type}', name: 'posts_list', methods: 'GET')]
#[PurgeOn(Post::class, routeParams: ['type' => new DynamicValues([MyClass::class, 'getValue'])])]
public function listAction()
{
}

final class MyClass
{
public static function getValue(Post $post)
{
// Return the desired value for the route parameter
}
}
```

[0]: https://github.com/sofascore/purgatory-bundle/blob/2.x/src/Attribute/AsRouteParamService.php
12 changes: 12 additions & 0 deletions docs/purge-subscriptions-using-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,16 @@ posts_list:
class: App\Entity\Post
route_params:
type: !dynamic [ my_service, prop ]

# Using values provided by a static method
posts_list:
class: App\Entity\Post
route_params:
type: !dynamic 'App\\MyClass::getValue'

# Using values provided by a static method with a property path
posts_list:
class: App\Entity\Post
route_params:
type: !dynamic [ 'App\\MyClass::getValue', prop ]
```
16 changes: 11 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
parameters:
ignoreErrors:
-
message: '#^Method Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\DynamicValues\:\:normalizeProvider\(\) should return \(array\<string\>&callable\(\)\: mixed\)\|string but returns non\-empty\-array\<object\|string\>&callable\(\)\: mixed\.$#'
identifier: return.type
count: 1
path: src/Attribute/RouteParamValue/DynamicValues.php

-
message: '#^Call to function is_a\(\) with arguments class\-string\<BackedEnum\>, ''BackedEnum'' and true will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
Expand All @@ -25,31 +31,31 @@ parameters:
path: src/Cache/PropertyResolver/AssociationResolver.php

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

-
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\.$#'
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\.$#'
identifier: argument.type
count: 1
path: src/Cache/RouteMetadata/YamlMetadataProvider.php

-
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\.$#'
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\.$#'
identifier: argument.type
count: 1
path: src/Cache/RouteMetadata/YamlMetadataProvider.php

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

-
message: '#^Parameter \#1 \$property of class Sofascore\\PurgatoryBundle\\Attribute\\RouteParamValue\\PropertyValues constructor expects string, bool\|float\|int\|string\|Symfony\\Component\\Yaml\\Tag\\TaggedValue given\.$#'
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\.$#'
identifier: argument.type
count: 1
path: src/Cache/RouteMetadata/YamlMetadataProvider.php
Expand Down
39 changes: 35 additions & 4 deletions src/Attribute/RouteParamValue/DynamicValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,55 @@
final class DynamicValues extends AbstractValues
{
/**
* @param string $alias Alias defined in {@see AsRouteParamService} attribute
* @var string|callable-array<string>
*/
public readonly string|array $provider;

/**
* @param string|callable-array<string> $provider Alias defined in {@see AsRouteParamService} attribute or static method callable
*/
public function __construct(
public readonly string $alias,
string|array $provider,
public readonly ?string $propertyPath = null,
) {
$this->provider = self::normalizeProvider($provider);
}

/**
* @return non-empty-list<?string>
* @return array<string|callable-array<string>|null>
*/
protected function getValues(): array
{
return [$this->alias, $this->propertyPath];
return [$this->provider, $this->propertyPath];
}

public static function type(): string
{
return 'dynamic';
}

/**
* @param string|callable-array<string|object> $provider
*
* @return string|callable-array<string>
*/
private static function normalizeProvider(string|array $provider): string|array
{
if (\is_string($provider)) {
if (!str_contains($provider, '::')) {
return $provider;
}
$provider = explode('::', $provider);
}

if (!\is_callable($provider)) {
throw new \ValueError('Only static method callables are supported.');
}

if (!\is_string($provider[0])) {
throw new \ValueError('Object callables are not supported.');
}

return $provider;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static function for(): string
public function build(ValuesInterface $values, string $associationClass, string $associationTarget): ValuesInterface
{
return new DynamicValues(
alias: $values->alias,
provider: $values->provider,
propertyPath: null !== $values->propertyPath ? \sprintf('%s?.%s', $associationTarget, $values->propertyPath) : $associationTarget,
);
}
Expand Down
28 changes: 16 additions & 12 deletions src/RouteParamValueResolver/DynamicValuesResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

/**
* @implements ValuesResolverInterface<array{0: string, 1: ?string}>
* @implements ValuesResolverInterface<array{0: string|callable-array<string>, 1: ?string}>
*/
final class DynamicValuesResolver implements ValuesResolverInterface
{
Expand All @@ -34,23 +34,27 @@ public static function for(): string
*/
public function resolve(array $unresolvedValues, object $entity): array
{
[$alias, $propertyPath] = $unresolvedValues;

try {
/** @var \Closure $routeParamService */
$routeParamService = $this->routeParamServiceLocator->get($alias);
} catch (ServiceNotFoundException $e) {
throw new RuntimeException(\sprintf(
'A route parameter resolver service with the alias "%s" was not found. Did you forget to use the #[AsPurgatoryResolver] attribute on your service?',
$alias,
), previous: $e);
[$provider, $propertyPath] = $unresolvedValues;

if (\is_array($provider)) {
$routeParamProvider = $provider(...);
} else {
try {
/** @var \Closure $routeParamProvider */
$routeParamProvider = $this->routeParamServiceLocator->get($provider);
} catch (ServiceNotFoundException $e) {
throw new RuntimeException(\sprintf(
'A route parameter resolver service with the alias "%s" was not found. Did you forget to use the #[AsPurgatoryResolver] attribute on your service?',
$provider,
), previous: $e);
}
}

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

/** @var scalar|list<?scalar>|null $values */
$values = $routeParamService($arg);
$values = $routeParamProvider($arg);

return \is_array($values) ? $values : [$values];
}
Expand Down
2 changes: 2 additions & 0 deletions tests/Application/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,8 @@ public function testDynamicValues(): void
self::assertUrlIsPurged('/animal/for-rating/106'); // __invoke
self::assertUrlIsPurged('/animal/for-rating/126'); // __invoke
self::assertUrlIsPurged('/animal/for-rating/32'); // getOwnerRating
self::assertUrlIsPurged('/animal/for-rating/600'); // getOtherRating
self::assertUrlIsPurged('/animal/for-rating/375'); // getOtherRating

self::clearPurger();

Expand Down
5 changes: 5 additions & 0 deletions tests/Application/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Sofascore\PurgatoryBundle\Tests\Functional\TestApplication\Entity\Person;
use Sofascore\PurgatoryBundle\Tests\Functional\TestApplication\Entity\Vehicle;
use Sofascore\PurgatoryBundle\Tests\Functional\TestApplication\Enum\Country;
use Sofascore\PurgatoryBundle\Tests\Functional\TestApplication\Service\AnimalRatingCalculator;

final class ConfigurationTest extends AbstractKernelTestCase
{
Expand Down Expand Up @@ -393,6 +394,10 @@ public static function configurationWithTargetProvider(): iterable
'type' => DynamicValues::type(),
'values' => ['purgatory.animal_rating3', 'owner'],
],
[
'type' => DynamicValues::type(),
'values' => [[AnimalRatingCalculator::class, 'getOtherRating'], null],
],
],
],
],
Expand Down
52 changes: 52 additions & 0 deletions tests/Attribute/RouteParamValue/DynamicValuesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Sofascore\PurgatoryBundle\Tests\Attribute\RouteParamValue;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use Sofascore\PurgatoryBundle\Attribute\RouteParamValue\DynamicValues;

#[CoversClass(DynamicValues::class)]
final class DynamicValuesTest extends TestCase
{
#[TestWith(['alias', 'alias'])]
#[TestWith([[DummyStaticCallable::class, 'handle'], [DummyStaticCallable::class, 'handle']])]
#[TestWith([DummyStaticCallable::class.'::handle', [DummyStaticCallable::class, 'handle']])]
public function testValueNormalization(string|array $input, string|array $expected): void
{
self::assertSame($expected, (new DynamicValues($input))->provider);
}

public function testNonStaticStringCallableIsRejected(): void
{
$this->expectException(\ValueError::class);
$this->expectExceptionMessage('Only static method callables are supported.');

new DynamicValues(DummyInstanceCallable::class.'::handle');
}

public function testObjectCallableIsRejected(): void
{
$this->expectException(\ValueError::class);
$this->expectExceptionMessage('Object callables are not supported.');

new DynamicValues([new DummyInstanceCallable(), 'handle']);
}
}

final class DummyStaticCallable
{
public static function handle(): void
{
}
}

final class DummyInstanceCallable
{
public function handle(): void
{
}
}
2 changes: 1 addition & 1 deletion tests/Cache/PropertyResolver/InverseValuesBuildersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function testBuild(): void
expected: new CompoundValues(
new DynamicValues('alias', propertyPath: 'association'),
new DynamicValues(
alias: 'alias',
provider: 'alias',
propertyPath: 'association?.obj',
),
new EnumValues(DummyIntEnum::class),
Expand Down
12 changes: 12 additions & 0 deletions tests/Cache/RouteMetadata/Fixtures/DummyClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Sofascore\PurgatoryBundle\Tests\Cache\RouteMetadata\Fixtures;

class DummyClass
{
public static function getValues()
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ foo_bar:
- !enum Sofascore\PurgatoryBundle\Tests\Cache\RouteMetadata\Fixtures\DummyEnum
param5: !dynamic foo
param6: !dynamic [ foo, bar ]
param7: !expression 'constant("App\\Foo::MAP")[obj.geValue()]'
param7: !dynamic 'Sofascore\PurgatoryBundle\Tests\Cache\RouteMetadata\Fixtures\DummyClass::getValues'
param8: !dynamic [ 'Sofascore\PurgatoryBundle\Tests\Cache\RouteMetadata\Fixtures\DummyClass::getValues', 'bar' ]
param9: !dynamic [ [ 'Sofascore\PurgatoryBundle\Tests\Cache\RouteMetadata\Fixtures\DummyClass', 'getValues' ] ]
param10: !dynamic [ [ 'Sofascore\PurgatoryBundle\Tests\Cache\RouteMetadata\Fixtures\DummyClass', 'getValues' ], 'bar' ]
param11: !expression 'constant("App\\Foo::MAP")[obj.geValue()]'
7 changes: 6 additions & 1 deletion tests/Cache/RouteMetadata/YamlMetadataProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Sofascore\PurgatoryBundle\Exception\RuntimeException;
use Sofascore\PurgatoryBundle\Exception\UnknownYamlTagException;
use Sofascore\PurgatoryBundle\Listener\Enum\Action;
use Sofascore\PurgatoryBundle\Tests\Cache\RouteMetadata\Fixtures\DummyClass;
use Sofascore\PurgatoryBundle\Tests\Cache\RouteMetadata\Fixtures\DummyEnum;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Routing\Route;
Expand Down Expand Up @@ -121,7 +122,11 @@ public function testRouteMetadataWithTags(): void
),
'param5' => new DynamicValues('foo'),
'param6' => new DynamicValues('foo', 'bar'),
'param7' => new ExpressionValues('constant("App\\\\Foo::MAP")[obj.geValue()]'),
'param7' => new DynamicValues([DummyClass::class, 'getValues']),
'param8' => new DynamicValues([DummyClass::class, 'getValues'], 'bar'),
'param9' => new DynamicValues([DummyClass::class, 'getValues']),
'param10' => new DynamicValues([DummyClass::class, 'getValues'], 'bar'),
'param11' => new ExpressionValues('constant("App\\\\Foo::MAP")[obj.geValue()]'),
], $metadata[0]->purgeOn->routeParams);
self::assertNull($metadata[0]->purgeOn->if);
self::assertNull($metadata[0]->purgeOn->actions);
Expand Down
Loading