From 684ef61cde205485ce42a1ee8f6df87fa89922ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Thu, 20 Feb 2025 09:43:00 +0100 Subject: [PATCH 01/12] Add resource name collection --- .../Resource/ResourceNameCollection.php | 42 ++++++++++++++++++ .../Resource/ResourceNameCollectionTest.php | 43 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/Component/src/Metadata/Resource/ResourceNameCollection.php create mode 100644 src/Component/tests/Metadata/Resource/ResourceNameCollectionTest.php diff --git a/src/Component/src/Metadata/Resource/ResourceNameCollection.php b/src/Component/src/Metadata/Resource/ResourceNameCollection.php new file mode 100644 index 000000000..cec732846 --- /dev/null +++ b/src/Component/src/Metadata/Resource/ResourceNameCollection.php @@ -0,0 +1,42 @@ + + */ + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->names); + } + + public function count(): int + { + return \count($this->names); + } +} diff --git a/src/Component/tests/Metadata/Resource/ResourceNameCollectionTest.php b/src/Component/tests/Metadata/Resource/ResourceNameCollectionTest.php new file mode 100644 index 000000000..c482e6557 --- /dev/null +++ b/src/Component/tests/Metadata/Resource/ResourceNameCollectionTest.php @@ -0,0 +1,43 @@ +assertInstanceOf(\IteratorAggregate::class, $collection); + } + + public function testItIsCountable(): void + { + $collection = new ResourceNameCollection(); + + $this->assertInstanceOf(\Countable::class, $collection); + } + + public function testItIsACollectionOfResourceNames(): void + { + $collection = new ResourceNameCollection(['first_resource', 'second_resource']); + + $this->assertCount(2, $collection); + $this->assertEquals('first_resource', $collection->getIterator()[0]); + $this->assertEquals('second_resource', $collection->getIterator()[1]); + } +} From 59ffdaed249f478e45062cbf97c64029c5fe5562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Thu, 20 Feb 2025 14:23:04 +0100 Subject: [PATCH 02/12] Init resource name collection factory with attributes --- .../Resources/config/services/metadata.xml | 4 ++ .../services/metadata/resource_name.xml | 26 ++++++++ ...ttributesResourceNameCollectionFactory.php | 61 +++++++++++++++++++ ...ResourceNameCollectionFactoryInterface.php | 29 +++++++++ src/Component/tests/Dummy/DummyResource.php | 21 +++++++ ...butesResourceNameCollectionFactoryTest.php | 34 +++++++++++ 6 files changed, 175 insertions(+) create mode 100644 src/Bundle/Resources/config/services/metadata/resource_name.xml create mode 100644 src/Component/src/Metadata/Resource/Factory/AttributesResourceNameCollectionFactory.php create mode 100644 src/Component/src/Metadata/Resource/Factory/ResourceNameCollectionFactoryInterface.php create mode 100644 src/Component/tests/Dummy/DummyResource.php create mode 100644 src/Component/tests/Metadata/Resource/Factory/AttributesResourceNameCollectionFactoryTest.php diff --git a/src/Bundle/Resources/config/services/metadata.xml b/src/Bundle/Resources/config/services/metadata.xml index 18fea2a9b..29f98596c 100644 --- a/src/Bundle/Resources/config/services/metadata.xml +++ b/src/Bundle/Resources/config/services/metadata.xml @@ -12,6 +12,10 @@ --> + + + + diff --git a/src/Bundle/Resources/config/services/metadata/resource_name.xml b/src/Bundle/Resources/config/services/metadata/resource_name.xml new file mode 100644 index 000000000..75e3f0efa --- /dev/null +++ b/src/Bundle/Resources/config/services/metadata/resource_name.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + %sylius.resource.mapping% + + + + diff --git a/src/Component/src/Metadata/Resource/Factory/AttributesResourceNameCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/AttributesResourceNameCollectionFactory.php new file mode 100644 index 000000000..859070653 --- /dev/null +++ b/src/Component/src/Metadata/Resource/Factory/AttributesResourceNameCollectionFactory.php @@ -0,0 +1,61 @@ +decorated) { + foreach ($this->decorated->create() as $resourceClass) { + $classes[$resourceClass] = true; + } + } + + $paths = $this->mapping['paths'] ?? []; + + foreach (ClassReflection::getResourcesByPaths($paths) as $resourceClass) { + if ([] === ClassReflection::getClassAttributes($resourceClass, AsResource::class)) { + continue; + } + + $classes[$resourceClass] = true; + } + + return new ResourceNameCollection(array_keys($classes)); + } +} diff --git a/src/Component/src/Metadata/Resource/Factory/ResourceNameCollectionFactoryInterface.php b/src/Component/src/Metadata/Resource/Factory/ResourceNameCollectionFactoryInterface.php new file mode 100644 index 000000000..c35d9c2a5 --- /dev/null +++ b/src/Component/src/Metadata/Resource/Factory/ResourceNameCollectionFactoryInterface.php @@ -0,0 +1,29 @@ + [dirname(__DIR__, 3) . '/Dummy']], + ); + + $collection = $attributesResourceNameCollectionFactory->create(); + + $this->assertContains(DummyResource::class, $collection->getIterator()); + $this->assertNotContains(PullRequest::class, $collection->getIterator()); + } +} From 444010dfe6fb76c2bab9da555396ae652f6e9b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Fri, 21 Feb 2025 10:04:33 +0100 Subject: [PATCH 03/12] Add Resource Route Collection Factory --- psalm.xml | 9 +++ .../Resources/config/services/routing.xml | 4 + .../config/services/routing/loader.xml | 23 ++++++ .../config/services/routing/resource.xml | 23 ++++++ src/Component/composer.json | 1 + .../AttributesOperationRouteFactory.php | 6 ++ ...tributesOperationRouteFactoryInterface.php | 6 ++ .../ResourceRouteCollectionFactory.php | 78 +++++++++++++++++++ ...esourceRouteCollectionFactoryInterface.php | 25 ++++++ .../Symfony/Routing/Loader/ResourceLoader.php | 47 +++++++++++ .../ResourceRouteCollectionFactoryTest.php | 71 +++++++++++++++++ .../Routing/Loader/ResourceLoaderTest.php | 62 +++++++++++++++ .../config/routes/sylius_resource.yaml | 8 +- 13 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 src/Bundle/Resources/config/services/routing/loader.xml create mode 100644 src/Bundle/Resources/config/services/routing/resource.xml create mode 100644 src/Component/src/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactory.php create mode 100644 src/Component/src/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactoryInterface.php create mode 100644 src/Component/src/Symfony/Routing/Loader/ResourceLoader.php create mode 100644 src/Component/tests/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactoryTest.php create mode 100644 src/Component/tests/Symfony/Routing/Loader/ResourceLoaderTest.php diff --git a/psalm.xml b/psalm.xml index 2defa899f..81de9bc5f 100644 --- a/psalm.xml +++ b/psalm.xml @@ -46,11 +46,20 @@ + + + + + + + + + diff --git a/src/Bundle/Resources/config/services/routing.xml b/src/Bundle/Resources/config/services/routing.xml index 567598b92..702dc90e3 100644 --- a/src/Bundle/Resources/config/services/routing.xml +++ b/src/Bundle/Resources/config/services/routing.xml @@ -12,6 +12,10 @@ --> + + + + diff --git a/src/Bundle/Resources/config/services/routing/loader.xml b/src/Bundle/Resources/config/services/routing/loader.xml new file mode 100644 index 000000000..12b09fdbd --- /dev/null +++ b/src/Bundle/Resources/config/services/routing/loader.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/src/Bundle/Resources/config/services/routing/resource.xml b/src/Bundle/Resources/config/services/routing/resource.xml new file mode 100644 index 000000000..d114b0e1f --- /dev/null +++ b/src/Bundle/Resources/config/services/routing/resource.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/src/Component/composer.json b/src/Component/composer.json index 4cc4d95f5..3375a24de 100644 --- a/src/Component/composer.json +++ b/src/Component/composer.json @@ -34,6 +34,7 @@ "pagerfanta/core": "^3.7 || ^4.0", "symfony/event-dispatcher": "^6.4 || ^7.1", "symfony/form": "^6.4 || ^7.1", + "symfony/framework-bundle": "^6.4 || ^7.1", "symfony/http-foundation": "^6.4 || ^7.1", "symfony/http-kernel": "^6.4 || ^7.1", "symfony/property-access": "^6.4 || ^7.1", diff --git a/src/Component/src/Symfony/Routing/Factory/AttributesOperationRouteFactory.php b/src/Component/src/Symfony/Routing/Factory/AttributesOperationRouteFactory.php index 84ad9a2bd..9b55660e9 100644 --- a/src/Component/src/Symfony/Routing/Factory/AttributesOperationRouteFactory.php +++ b/src/Component/src/Symfony/Routing/Factory/AttributesOperationRouteFactory.php @@ -19,10 +19,16 @@ use Sylius\Resource\Metadata\RegistryInterface; use Sylius\Resource\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use Sylius\Resource\Metadata\ResourceMetadata; +use Sylius\Resource\Symfony\Routing\Factory\Resource\ResourceRouteCollectionFactory; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use Webmozart\Assert\Assert; +trigger_deprecation('sylius/resource', '1.13', '"%s" is deprecated, use "%s" instead.', AttributesOperationRouteFactory::class, ResourceRouteCollectionFactory::class); + +/** + * @deprecated + */ final class AttributesOperationRouteFactory implements AttributesOperationRouteFactoryInterface { public function __construct( diff --git a/src/Component/src/Symfony/Routing/Factory/AttributesOperationRouteFactoryInterface.php b/src/Component/src/Symfony/Routing/Factory/AttributesOperationRouteFactoryInterface.php index e950865df..89745f305 100644 --- a/src/Component/src/Symfony/Routing/Factory/AttributesOperationRouteFactoryInterface.php +++ b/src/Component/src/Symfony/Routing/Factory/AttributesOperationRouteFactoryInterface.php @@ -13,8 +13,14 @@ namespace Sylius\Resource\Symfony\Routing\Factory; +use Sylius\Resource\Symfony\Routing\Factory\Resource\ResourceRouteCollectionFactoryInterface; use Symfony\Component\Routing\RouteCollection; +trigger_deprecation('sylius/resource', '1.13', '"%s" is deprecated, use "%s" instead.', AttributesOperationRouteFactoryInterface::class, ResourceRouteCollectionFactoryInterface::class); + +/** + * @deprecated + */ interface AttributesOperationRouteFactoryInterface { /** @psalm-param class-string $className */ diff --git a/src/Component/src/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactory.php b/src/Component/src/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactory.php new file mode 100644 index 000000000..153de58b1 --- /dev/null +++ b/src/Component/src/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactory.php @@ -0,0 +1,78 @@ +resourceMetadataFactory->create($className); + + /** @var ResourceMetadata $resource */ + foreach ($resourceMetadata->getIterator() as $resource) { + $this->createRoutesForResource($routeCollection, $resource); + } + + return $routeCollection; + } + + private function createRoutesForResource(RouteCollection $routeCollection, ResourceMetadata $resource): void + { + foreach ($resource->getOperations() ?? new Operations() as $operation) { + if (!$operation instanceof HttpOperation) { + continue; + } + + $this->addRouteForOperation($routeCollection, $resource, $operation); + } + } + + private function addRouteForOperation(RouteCollection $routeCollection, ResourceMetadata $resource, HttpOperation $operation): void + { + $metadata = $this->resourceRegistry->get($resource->getAlias() ?? ''); + $routeName = $operation->getRouteName(); + + Assert::notNull($routeName, sprintf('Operation %s has no route name. Please define one.', $operation::class)); + + $route = $this->createRoute($metadata, $resource, $operation); + $routeCollection->add($routeName, $route); + } + + private function createRoute(MetadataInterface $metadata, ResourceMetadata $resource, HttpOperation $operation): Route + { + return $this->operationRouteFactory->create($metadata, $resource, $operation); + } +} diff --git a/src/Component/src/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactoryInterface.php b/src/Component/src/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactoryInterface.php new file mode 100644 index 000000000..e1a44e682 --- /dev/null +++ b/src/Component/src/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactoryInterface.php @@ -0,0 +1,25 @@ +resourceNameCollectionFactory->create(); + + /** + * @var class-string $resourceName + */ + foreach ($resourceNames as $resourceName) { + $routeCollection->addCollection($this->resourceRouteCollectionFactory->createRouteCollectionForClass($resourceName)); + } + + return $routeCollection; + } +} diff --git a/src/Component/tests/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactoryTest.php b/src/Component/tests/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactoryTest.php new file mode 100644 index 000000000..a7768a413 --- /dev/null +++ b/src/Component/tests/Symfony/Routing/Factory/Resource/ResourceRouteCollectionFactoryTest.php @@ -0,0 +1,71 @@ +resourceRegistry = $this->createMock(RegistryInterface::class); + $this->routePathFactory = $this->createMock(OperationRoutePathFactoryInterface::class); + + $this->factory = new ResourceRouteCollectionFactory( + new OperationRouteFactory($this->routePathFactory), + new AttributesResourceMetadataCollectionFactory( + $this->resourceRegistry, + new OperationRouteNameFactory(), + ), + $this->resourceRegistry, + ); + } + + public function testItCreatesRoutesWithOperations(): void + { + $metadata = $this->createMock(MetadataInterface::class); + $metadata->method('getServiceId')->with('repository')->willReturn('app.repository.dummy'); + $metadata->method('getClass')->willReturnMap([ + ['form', 'App\Form'], + ['model', 'App\Dummy'], + ]); + $metadata->method('getApplicationName')->willReturn('app'); + $metadata->method('getName')->willReturn('dummy'); + $metadata->method('getPluralName')->willReturn('dummies'); + + $this->resourceRegistry->method('get')->with('app.dummy')->willReturn($metadata); + + $routeCollection = $this->factory->createRouteCollectionForClass(DummyResourceWithOperations::class); + + $this->assertEquals(4, $routeCollection->count()); + $this->assertNotNull($routeCollection->get('app_dummy_index'), 'Route "app_dummy_index" not found but it should.'); + $this->assertNotNull($routeCollection->get('app_dummy_create'), 'Route "app_dummy_create" not found but it should.'); + $this->assertNotNull($routeCollection->get('app_dummy_update'), 'Route "app_dummy_update" not found but it should.'); + $this->assertNotNull($routeCollection->get('app_dummy_show'), 'Route "app_dummy_show" not found but it should.'); + } +} diff --git a/src/Component/tests/Symfony/Routing/Loader/ResourceLoaderTest.php b/src/Component/tests/Symfony/Routing/Loader/ResourceLoaderTest.php new file mode 100644 index 000000000..3846a2c97 --- /dev/null +++ b/src/Component/tests/Symfony/Routing/Loader/ResourceLoaderTest.php @@ -0,0 +1,62 @@ +resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class); + $this->resourceRouteCollectionFactory = $this->createMock(ResourceRouteCollectionFactoryInterface::class); + + $this->loader = new ResourceLoader( + $this->resourceNameCollectionFactory, + $this->resourceRouteCollectionFactory, + ); + } + + public function testItIsARouteLoader(): void + { + $this->assertInstanceOf(RouteLoaderInterface::class, $this->loader); + } + + public function testItGeneratesRoutesFromResource(): void + { + $routeCollection = new RouteCollection(); + $routeCollection->add('first_route', new Route('/first-route')); + $routeCollection->add('second_route', new Route('/second-route')); + + $resourceNameCollection = new ResourceNameCollection(['\DummyClass']); + + $this->resourceNameCollectionFactory->method('create')->willReturn($resourceNameCollection); + $this->resourceRouteCollectionFactory->method('createRouteCollectionForClass')->with('\DummyClass')->willReturn($routeCollection); + + $this->assertEquals($routeCollection, ($this->loader)()); + } +} diff --git a/tests/Application/config/routes/sylius_resource.yaml b/tests/Application/config/routes/sylius_resource.yaml index 0c8ade2c2..04dfe3569 100644 --- a/tests/Application/config/routes/sylius_resource.yaml +++ b/tests/Application/config/routes/sylius_resource.yaml @@ -1,7 +1,11 @@ sylius_crud_routes: resource: 'sylius.routing.loader.crud_routes_attributes' type: service +# +#sylius_routes: +# resource: 'sylius.routing.loader.routes_attributes' +# type: service -sylius_routes: - resource: 'sylius.routing.loader.routes_attributes' +sylius_resource_routes: + resource: 'sylius_resource.symfony.routing.loader.resource' type: service From d00a2c07cd79376bf19052536f19fe962f8a772a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Mon, 17 Feb 2025 15:21:01 +0100 Subject: [PATCH 04/12] Use Repository managed by Doctrine --- .../services/integrations/doctrine/orm.xml | 18 ++ .../Resources/config/services/metadata.xml | 9 - .../Resources/config/services/state.xml | 7 - .../Symfony/Request/State/ProviderSpec.php | 175 ------------------ ...trineResourceMetadataCollectionFactory.php | 2 + ...neORMResourceMetadataCollectionFactory.php | 106 +++++++++++ .../ORM}/State/Provider.php | 87 ++++++--- .../src/Grid/State/RequestGridProvider.php | 4 +- src/Component/src/Metadata/AsResource.php | 3 + ...butesResourceMetadataCollectionFactory.php | 8 +- ...viderResourceMetadataCollectionFactory.php | 5 - .../src/Metadata/ResourceMetadata.php | 13 ++ ...MResourceMetadataCollectionFactoryTest.php | 123 ++++++++++++ .../tests/Doctrine/ORM/State/ProviderTest.php | 116 ++++++++++++ ...sResourceMetadataCollectionFactoryTest.php | 3 - ...rResourceMetadataCollectionFactoryTest.php | 22 --- tests/Application/config/services.yaml | 3 - .../src/Subscription/Entity/Subscription.php | 8 +- .../Factory/SubscriptionFactory.php | 5 +- .../Repository/SubscriptionRepository.php | 29 +++ .../Tests/Controller/SubscriptionUiTest.php | 19 +- 21 files changed, 494 insertions(+), 271 deletions(-) delete mode 100644 src/Component/spec/Symfony/Request/State/ProviderSpec.php create mode 100644 src/Component/src/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactory.php rename src/Component/src/{Symfony/Request => Doctrine/ORM}/State/Provider.php (52%) create mode 100644 src/Component/tests/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactoryTest.php create mode 100644 src/Component/tests/Doctrine/ORM/State/ProviderTest.php create mode 100644 tests/Application/src/Subscription/Repository/SubscriptionRepository.php diff --git a/src/Bundle/Resources/config/services/integrations/doctrine/orm.xml b/src/Bundle/Resources/config/services/integrations/doctrine/orm.xml index 0ca399cfe..5f19df807 100644 --- a/src/Bundle/Resources/config/services/integrations/doctrine/orm.xml +++ b/src/Bundle/Resources/config/services/integrations/doctrine/orm.xml @@ -20,6 +20,24 @@ + + + + + + + + + + + + + + diff --git a/src/Bundle/Resources/config/services/metadata.xml b/src/Bundle/Resources/config/services/metadata.xml index 29f98596c..b6a0110fc 100644 --- a/src/Bundle/Resources/config/services/metadata.xml +++ b/src/Bundle/Resources/config/services/metadata.xml @@ -38,15 +38,6 @@ %sylius.state_machine_component.default% - - - - - - - - - - - - diff --git a/src/Component/spec/Symfony/Request/State/ProviderSpec.php b/src/Component/spec/Symfony/Request/State/ProviderSpec.php deleted file mode 100644 index 05d3fbb89..000000000 --- a/src/Component/spec/Symfony/Request/State/ProviderSpec.php +++ /dev/null @@ -1,175 +0,0 @@ -beConstructedWith($locator, new RepositoryArgumentResolver(), $argumentParser); - } - - function it_is_initializable(): void - { - $this->shouldHaveType(Provider::class); - } - - function it_calls_repository_as_callable( - Operation $operation, - Request $request, - ): void { - $operation->getRepository()->willReturn([RepositoryWithCallables::class, 'find']); - $operation->getRepositoryArguments()->willReturn(null); - - $request->attributes = new ParameterBag(['_route_params' => ['id' => 'my_id']]); - $request->query = new InputBag([]); - $request->request = new InputBag(); - - $response = $this->provide($operation, new Context(new RequestOption($request->getWrappedObject()))); - $response->shouldHaveType(\stdClass::class); - $response->id->shouldReturn('my_id'); - } - - function it_calls_repository_as_string( - Operation $operation, - Request $request, - ContainerInterface $locator, - RepositoryInterface $repository, - \stdClass $stdClass, - ): void { - $operation->getRepository()->willReturn('App\Repository'); - $operation->getRepositoryMethod()->willReturn(null); - $operation->getRepositoryArguments()->willReturn(null); - - $request->attributes = new ParameterBag(['_route_params' => ['id' => 'my_id', '_sylius' => ['resource' => 'app.dummy']]]); - $request->query = new InputBag([]); - $request->request = new InputBag(); - - $locator->has('App\Repository')->willReturn(true); - $locator->get('App\Repository')->willReturn($repository); - - $repository->findOneBy(['id' => 'my_id'])->willReturn($stdClass); - - $response = $this->provide($operation, new Context(new RequestOption($request->getWrappedObject()))); - $response->shouldReturn($stdClass); - } - - function it_calls_create_paginator_by_default_on_collection_operations( - Request $request, - ContainerInterface $locator, - RepositoryInterface $repository, - Pagerfanta $pagerfanta, - ): void { - $operation = new Index(repository: 'App\Repository'); - - $request->attributes = new ParameterBag(['_route_params' => ['id' => 'my_id', '_sylius' => ['resource' => 'app.dummy']]]); - $request->query = new InputBag([]); - $request->request = new InputBag(); - - $locator->has('App\Repository')->willReturn(true); - $locator->get('App\Repository')->willReturn($repository); - - $repository->createPaginator()->willReturn($pagerfanta)->shouldBeCalled(); - $pagerfanta->setCurrentPage(1)->willReturn($pagerfanta)->shouldBeCalled(); - - $response = $this->provide($operation, new Context(new RequestOption($request->getWrappedObject()))); - $response->shouldReturn($pagerfanta); - } - - function it_sets_current_page_from_request_when_data_is_a_paginator( - Request $request, - ContainerInterface $locator, - RepositoryInterface $repository, - Pagerfanta $pagerfanta, - ): void { - $operation = new Index(repository: 'App\Repository'); - - $request->attributes = new ParameterBag(['_route_params' => ['id' => 'my_id', '_sylius' => ['resource' => 'app.dummy']]]); - $request->query = new InputBag(['page' => 42]); - $request->request = new InputBag(); - - $locator->has('App\Repository')->willReturn(true); - $locator->get('App\Repository')->willReturn($repository); - - $repository->createPaginator()->willReturn($pagerfanta)->shouldBeCalled(); - $pagerfanta->setCurrentPage(42)->willReturn($pagerfanta)->shouldBeCalled(); - - $response = $this->provide($operation, new Context(new RequestOption($request->getWrappedObject()))); - $response->shouldReturn($pagerfanta); - $pagerfanta->getCurrentPage()->willReturn(42); - } - - function it_calls_repository_as_string_with_specific_repository_method( - Operation $operation, - Request $request, - ContainerInterface $locator, - RepositoryInterface $repository, - \stdClass $stdClass, - ): void { - $operation->getRepository()->willReturn('App\Repository'); - $operation->getRepositoryMethod()->willReturn('find'); - $operation->getRepositoryArguments()->willReturn(null); - - $request->attributes = new ParameterBag(['_route_params' => ['id' => 'my_id', '_sylius' => ['resource' => 'app.dummy']]]); - $request->query = new InputBag([]); - $request->request = new InputBag(); - - $locator->has('App\Repository')->willReturn(true); - $locator->get('App\Repository')->willReturn($repository); - - $repository->find('my_id')->willReturn($stdClass); - - $response = $this->provide($operation, new Context(new RequestOption($request->getWrappedObject()))); - $response->shouldReturn($stdClass); - } - - function it_calls_repository_as_string_with_specific_repository_method_an_arguments( - Operation $operation, - Request $request, - ContainerInterface $locator, - RepositoryInterface $repository, - ArgumentParserInterface $argumentParser, - \stdClass $stdClass, - ): void { - $operation->getRepository()->willReturn('App\Repository'); - $operation->getRepositoryMethod()->willReturn('find'); - $operation->getRepositoryArguments()->willReturn(['id' => "request.attributes.get('id')"]); - - $argumentParser->parseExpression("request.attributes.get('id')")->willReturn('my_id'); - - $locator->has('App\Repository')->willReturn(true); - $locator->get('App\Repository')->willReturn($repository); - - $repository->find('my_id')->willReturn($stdClass); - - $response = $this->provide($operation, new Context(new RequestOption($request->getWrappedObject()))); - $response->shouldReturn($stdClass); - } -} diff --git a/src/Component/src/Doctrine/Common/Metadata/Resource/Factory/DoctrineResourceMetadataCollectionFactory.php b/src/Component/src/Doctrine/Common/Metadata/Resource/Factory/DoctrineResourceMetadataCollectionFactory.php index b6141ac35..dce1e9865 100644 --- a/src/Component/src/Doctrine/Common/Metadata/Resource/Factory/DoctrineResourceMetadataCollectionFactory.php +++ b/src/Component/src/Doctrine/Common/Metadata/Resource/Factory/DoctrineResourceMetadataCollectionFactory.php @@ -15,6 +15,7 @@ use Sylius\Resource\Doctrine\Common\State\PersistProcessor; use Sylius\Resource\Doctrine\Common\State\RemoveProcessor; +use Sylius\Resource\Doctrine\ORM\Metadata\Resource\Factory\DoctrineORMResourceMetadataCollectionFactory; use Sylius\Resource\Metadata\DeleteOperationInterface; use Sylius\Resource\Metadata\Operation; use Sylius\Resource\Metadata\Operations; @@ -29,6 +30,7 @@ public function __construct( private RegistryInterface $resourceRegistry, private ResourceMetadataCollectionFactoryInterface $decorated, ) { + trigger_deprecation('sylius/resource', '1.13', 'The "%s" is deprecated use "%s instead.', self::class, DoctrineORMResourceMetadataCollectionFactory::class); } public function create(string $resourceClass): ResourceMetadataCollection diff --git a/src/Component/src/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactory.php b/src/Component/src/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactory.php new file mode 100644 index 000000000..6e76dbf9f --- /dev/null +++ b/src/Component/src/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactory.php @@ -0,0 +1,106 @@ +decorated->create($resourceClass); + + /** @var ResourceMetadata $resource */ + foreach ($resourceCollectionMetadata->getIterator() as $i => $resource) { + $operations = $resource->getOperations() ?? new Operations(); + + $entityClass = $resource->getClass(); + + if (null === $entityClass) { + continue; + } + + /** @var Operation $operation */ + foreach ($operations as $operation) { + /** @var string $key */ + $key = $operation->getName(); + + $entityManager = $this->managerRegistry->getManagerForClass($entityClass); + + if (!$entityManager instanceof EntityManagerInterface) { + $operations->add($key, $operation); + + continue; + } + + $operations->add($key, $this->addDefaults($operation)); + } + + $resource = $resource->withOperations($operations); + + $resourceCollectionMetadata[$i] = $resource; + } + + return $resourceCollectionMetadata; + } + + private function addDefaults(Operation $operation): Operation + { + $operation = $operation->withProvider($this->getProvider($operation)); + + return $operation->withProcessor($this->getProcessor($operation)); + } + + private function getProvider(Operation $operation): callable|string|null + { + if (null !== $provider = $operation->getProvider()) { + return $provider; + } + + if ($operation instanceof GridAwareOperationInterface && null !== $operation->getGrid()) { + return null; + } + + return 'sylius.state_provider.doctrine.orm.state.provider'; + } + + private function getProcessor(Operation $operation): callable|string + { + if (null !== $processor = $operation->getProcessor()) { + return $processor; + } + + if ($operation instanceof DeleteOperationInterface) { + return RemoveProcessor::class; + } + + return PersistProcessor::class; + } +} diff --git a/src/Component/src/Symfony/Request/State/Provider.php b/src/Component/src/Doctrine/ORM/State/Provider.php similarity index 52% rename from src/Component/src/Symfony/Request/State/Provider.php rename to src/Component/src/Doctrine/ORM/State/Provider.php index cea4eda91..cac55efa3 100644 --- a/src/Component/src/Symfony/Request/State/Provider.php +++ b/src/Component/src/Doctrine/ORM/State/Provider.php @@ -11,9 +11,11 @@ declare(strict_types=1); -namespace Sylius\Resource\Symfony\Request\State; +namespace Sylius\Resource\Doctrine\ORM\State; -use Pagerfanta\Pagerfanta; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; +use Pagerfanta\PagerfantaInterface; use Psr\Container\ContainerInterface; use Sylius\Resource\Context\Context; use Sylius\Resource\Context\Option\RequestOption; @@ -31,49 +33,33 @@ final class Provider implements ProviderInterface { public function __construct( - private ContainerInterface $locator, + private ManagerRegistry $managerRegistry, private RepositoryArgumentResolver $argumentResolver, private ArgumentParserInterface $argumentParser, + private ContainerInterface $locator, ) { } public function provide(Operation $operation, Context $context): object|array|null { $request = $context->get(RequestOption::class)?->request(); - $repository = $operation->getRepository(); - if ( - null === $request || - null === $repository - ) { + if (null === $request) { return null; } + $repository = $operation->getRepository(); $repositoryInstance = null; - $arguments = $this->parseArgumentValues($operation->getRepositoryArguments() ?? []); - - if (\is_string($repository)) { - $defaultMethod = $operation instanceof CollectionOperationInterface ? 'createPaginator' : 'findOneBy'; - - if ($operation instanceof BulkOperationInterface) { - $defaultMethod = 'findById'; - } - - $method = $operation->getRepositoryMethod() ?? $defaultMethod; - if (!$this->locator->has($repository)) { - throw new \RuntimeException(sprintf('Repository "%s" not found on operation "%s"', $repository, $operation->getName() ?? '')); - } - - $repositoryInstance = $this->locator->get($repository); - - // make it as callable - /** @var callable $repository */ - $repository = [$repositoryInstance, $method]; + if (\is_callable($repository)) { + $callableRepository = $repository; + } else { + $repositoryInstance = $this->getRepositoryInstance($operation); + $callableRepository = $this->createCallableRepository($operation, $repositoryInstance); } try { - $reflector = CallableReflection::from($repository); + $reflector = CallableReflection::from($callableRepository); } catch (\ReflectionException $exception) { if (null === $repositoryInstance) { throw $exception; @@ -85,13 +71,15 @@ public function provide(Operation $operation, Context $context): object|array|nu $reflector = CallableReflection::from($callable); } + $arguments = $this->parseArgumentValues($operation->getRepositoryArguments() ?? []); + if ([] === $arguments) { $arguments = $this->argumentResolver->getArguments($request, $reflector); } - $data = $repository(...$arguments); + $data = $callableRepository(...$arguments); - if ($data instanceof Pagerfanta) { + if ($data instanceof PagerfantaInterface) { $currentPage = $request->query->getInt('page', 1); $data->setCurrentPage($currentPage); } @@ -107,4 +95,43 @@ private function parseArgumentValues(array $arguments): array return $arguments; } + + private function createCallableRepository(Operation $operation, mixed $repositoryInstance): callable + { + $defaultMethod = $operation instanceof CollectionOperationInterface ? 'createPaginator' : 'findOneBy'; + + if ($operation instanceof BulkOperationInterface) { + $defaultMethod = 'findById'; + } + + $method = $operation->getRepositoryMethod() ?? $defaultMethod; + + // make it as callable + /** @var callable $repository */ + $repository = [$repositoryInstance, $method]; + + return $repository; + } + + private function getRepositoryInstance(Operation $operation): mixed + { + /** @var string|null $repository */ + $repository = $operation->getRepository(); + + if (null === $repository) { + /** @var class-string $entityClass */ + $entityClass = $operation->getResource()?->getClass(); + + /** @var EntityManagerInterface $manager */ + $manager = $this->managerRegistry->getManagerForClass($entityClass); + + return $manager->getRepository($entityClass); + } + + if (!$this->locator->has($repository)) { + throw new \RuntimeException(sprintf('Repository "%s" not found on operation "%s"', $repository, $operation->getName() ?? '')); + } + + return $this->locator->get($repository); + } } diff --git a/src/Component/src/Grid/State/RequestGridProvider.php b/src/Component/src/Grid/State/RequestGridProvider.php index 8b5311840..79237872f 100644 --- a/src/Component/src/Grid/State/RequestGridProvider.php +++ b/src/Component/src/Grid/State/RequestGridProvider.php @@ -13,7 +13,7 @@ namespace Sylius\Resource\Grid\State; -use Pagerfanta\Pagerfanta; +use Pagerfanta\PagerfantaInterface; use Sylius\Component\Grid\Parameters; use Sylius\Component\Grid\Provider\GridProviderInterface; use Sylius\Resource\Context\Context; @@ -63,7 +63,7 @@ public function provide(Operation $operation, Context $context): object|array|nu $data = $gridView->getData(); - if ($data instanceof Pagerfanta) { + if ($data instanceof PagerfantaInterface) { $currentPage = $request->query->getInt('page', 1); $data->setCurrentPage($currentPage); diff --git a/src/Component/src/Metadata/AsResource.php b/src/Component/src/Metadata/AsResource.php index f2eb89938..df0925b2e 100644 --- a/src/Component/src/Metadata/AsResource.php +++ b/src/Component/src/Metadata/AsResource.php @@ -16,6 +16,9 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] final class AsResource { + /** + * @param class-string|null $class + */ public function __construct( private ?string $alias = null, private ?string $section = null, diff --git a/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index 23e4d1de8..0ae0097c1 100644 --- a/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -48,6 +48,7 @@ public function create(string $resourceClass): ResourceMetadataCollection /** * @param \ReflectionAttribute[] $attributes + * @param class-string $resourceClass * * @return ResourceMetadata[] */ @@ -118,6 +119,9 @@ private function buildResourceOperations(array $attributes, string $resourceClas return $resources; } + /** + * @param class-string $resourceClass + */ private function getResourceWithDefaults(string $resourceClass, ResourceMetadata $resource, MetadataInterface $resourceConfiguration): ResourceMetadata { $resource = $resource->withClass($resourceClass); @@ -171,10 +175,6 @@ private function getOperationWithDefaults(ResourceMetadata $resource, Operation $operation = $operation->withResource($resource); - if (null === $operation->getRepository()) { - $operation = $operation->withRepository($resourceConfiguration->getServiceId('repository')); - } - if (null === $operation->getFormType()) { $formType = $resource->getFormType() ?? $resourceConfiguration->getClass('form'); $operation = $operation->withFormType($formType); diff --git a/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php index f5f57d1fe..27ff1ec3f 100644 --- a/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php +++ b/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php @@ -19,7 +19,6 @@ use Sylius\Resource\Metadata\Operations; use Sylius\Resource\Metadata\Resource\ResourceMetadataCollection; use Sylius\Resource\Metadata\ResourceMetadata; -use Sylius\Resource\Symfony\Request\State\Provider; final class ProviderResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { @@ -62,10 +61,6 @@ private function addDefaults(Operation $operation): Operation $operation = $operation->withProvider(RequestGridProvider::class); } - if (null === $operation->getProvider()) { - $operation = $operation->withProvider(Provider::class); - } - return $operation; } } diff --git a/src/Component/src/Metadata/ResourceMetadata.php b/src/Component/src/Metadata/ResourceMetadata.php index c2ee0a9c3..d9ffcd479 100644 --- a/src/Component/src/Metadata/ResourceMetadata.php +++ b/src/Component/src/Metadata/ResourceMetadata.php @@ -17,6 +17,9 @@ final class ResourceMetadata { private ?Operations $operations; + /** + * @param class-string|null $class + */ public function __construct( private ?string $alias = null, private ?string $section = null, @@ -36,13 +39,23 @@ public function __construct( ?array $operations = null, ) { $this->operations = null === $operations ? null : new Operations($operations); + + if (null !== $driver && false !== $driver) { + trigger_deprecation('sylius/resource', '1.13', 'Using driver is deprecated. If your resource is managed by Doctrine you have nothing to do, otherwise use a customer provider.'); + } } + /** + * @return class-string|null + */ public function getClass(): ?string { return $this->class; } + /** + * @param class-string $class + */ public function withClass(string $class): self { $self = clone $this; diff --git a/src/Component/tests/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactoryTest.php b/src/Component/tests/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactoryTest.php new file mode 100644 index 000000000..a760f8cd5 --- /dev/null +++ b/src/Component/tests/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactoryTest.php @@ -0,0 +1,123 @@ +managerRegistry = $this->prophesize(ManagerRegistry::class); + $this->decorated = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + + $this->factory = new DoctrineORMResourceMetadataCollectionFactory( + $this->managerRegistry->reveal(), + $this->decorated->reveal(), + ); + } + + public function testItIsInitializable(): void + { + $this->assertInstanceOf(DoctrineORMResourceMetadataCollectionFactory::class, $this->factory); + } + + public function testItAddsPersistProcessorToOperationsForResourceManagedByDoctrineOrm(): void + { + $entityManager = $this->createMock(EntityManagerInterface::class); + + $operation = new Create(name: 'app_dummy_create'); + $resource = (new ResourceMetadata(alias: 'app.dummy')) + ->withOperations(new Operations([$operation])) + ->withClass('App\Dummy') + ; + + $resourceMetadataCollection = new ResourceMetadataCollection([$resource]); + + $this->decorated->create('App\Resource')->willReturn($resourceMetadataCollection); + $this->managerRegistry->getManagerForClass('App\Dummy')->willReturn($entityManager); + + $result = $this->factory->create('App\Resource'); + + $this->assertEquals( + PersistProcessor::class, + $result->getOperation('app.dummy', 'app_dummy_create')->getProcessor(), + ); + } + + public function testItAddsRemoveProcessorToDeleteOperationsForResourceManagedByDoctrineOrm(): void + { + $entityManager = $this->createMock(EntityManagerInterface::class); + + $operation = new Delete(name: 'app_dummy_delete'); + $resource = (new ResourceMetadata(alias: 'app.dummy')) + ->withOperations(new Operations([$operation])) + ->withClass('App\Dummy') + ; + + $resourceMetadataCollection = new ResourceMetadataCollection([$resource]); + + $this->decorated->create('App\Resource')->willReturn($resourceMetadataCollection); + $this->managerRegistry->getManagerForClass('App\Dummy')->willReturn($entityManager); + + $result = $this->factory->create('App\Resource'); + + $this->assertEquals( + RemoveProcessor::class, + $result->getOperation('app.dummy', 'app_dummy_delete')->getProcessor(), + ); + } + + public function testItDoesNothingWhenResourceIsNotManagedByDoctrineOrm(): void + { + $operation = new Create(name: 'app_dummy_create'); + $resource = (new ResourceMetadata(alias: 'app.dummy')) + ->withOperations(new Operations([$operation])) + ->withClass('App\Dummy') + ; + + $resourceMetadataCollection = new ResourceMetadataCollection([$resource]); + + $this->decorated->create('App\Resource')->willReturn($resourceMetadataCollection); + $this->managerRegistry->getManagerForClass('App\Dummy')->willReturn(null); + + $result = $this->factory->create('App\Resource'); + + $this->assertEquals( + null, + $result->getOperation('app.dummy', 'app_dummy_create')->getProcessor(), + ); + } +} diff --git a/src/Component/tests/Doctrine/ORM/State/ProviderTest.php b/src/Component/tests/Doctrine/ORM/State/ProviderTest.php new file mode 100644 index 000000000..4e39b5f4f --- /dev/null +++ b/src/Component/tests/Doctrine/ORM/State/ProviderTest.php @@ -0,0 +1,116 @@ +managerRegistry = $this->createMock(ManagerRegistry::class); + $this->locator = $this->createMock(ContainerInterface::class); + $this->argumentParser = $this->createMock(ArgumentParserInterface::class); + $this->provider = new Provider($this->managerRegistry, new RepositoryArgumentResolver(), $this->argumentParser, $this->locator); + } + + public function testItCallsRepositoryFromDoctrineManagerRegistry(): void + { + $operation = $this->createMock(Operation::class); + $entityManager = $this->createMock(EntityManagerInterface::class); + $unitOfWork = $this->createMock(UnitOfWork::class); + $entityPersister = $this->createMock(EntityPersister::class); + + $operation->method('getRepository')->willReturn(null); + $operation->method('getRepositoryArguments')->willReturn(null); + $operation->method('getResource')->willReturn((new ResourceMetadata())->withClass('App\Dummy')); + + $this->managerRegistry->method('getManagerForClass')->with('App\Dummy')->willReturn($entityManager); + $entityRepository = new EntityRepository($entityManager, new ClassMetadata('App\Dummy')); + $entityManager->method('getRepository')->willReturn($entityRepository); + + $entityManager->method('getUnitOfWork')->willReturn($unitOfWork); + $unitOfWork->method('getEntityPersister')->willReturn($entityPersister); + + $expectedResult = (object) ['id' => 'my_id']; + $entityPersister->method('load')->with(['id' => 'my_id'], null, null, [], null, 1)->willReturn($expectedResult); + + $request = new Request([], [], ['_route_params' => ['id' => 'my_id']]); + + $response = $this->provider->provide($operation, new Context(new RequestOption($request))); + $this->assertEquals($expectedResult, $response); + } + + public function testItCallsRepositoryAsCallable(): void + { + $operation = $this->createMock(Operation::class); + $operation->method('getRepository')->willReturn([RepositoryWithCallables::class, 'find']); + $operation->method('getRepositoryArguments')->willReturn(null); + + $request = new Request([], [], ['_route_params' => ['id' => 'my_id']]); + + $response = $this->provider->provide($operation, new Context(new RequestOption($request))); + $this->assertInstanceOf(\stdClass::class, $response); + $this->assertEquals('my_id', $response->id); + } + + public function testItCallsRepositoryAsString(): void + { + $operation = $this->createMock(Operation::class); + $operation->method('getRepository')->willReturn('App\\Repository'); + $operation->method('getRepositoryMethod')->willReturn(null); + $operation->method('getRepositoryArguments')->willReturn(null); + + $request = new Request([], [], ['_route_params' => ['id' => 'my_id', '_sylius' => ['resource' => 'app.dummy']]]); + + $repository = $this->createMock(RepositoryInterface::class); + $stdClass = new \stdClass(); + + $this->locator->method('has')->with('App\\Repository')->willReturn(true); + $this->locator->method('get')->with('App\\Repository')->willReturn($repository); + $repository->method('findOneBy')->with(['id' => 'my_id'])->willReturn($stdClass); + + $response = $this->provider->provide($operation, new Context(new RequestOption($request))); + $this->assertSame($stdClass, $response); + } +} diff --git a/src/Component/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/src/Component/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index 86b64c378..d5c307812 100644 --- a/src/Component/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/src/Component/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -156,21 +156,18 @@ public function testItCreatesMultiResourcesMetadataWithOperations(): void $this->assertInstanceOf(Index::class, $operation); $this->assertSame('app_order_index', $operation->getName()); $this->assertSame(['GET'], $operation->getMethods()); - $this->assertSame('app.repository.order', $operation->getRepository()); $this->assertSame('App\Form\OrderType', $operation->getFormType()); $operation = $metadataCollection->getOperation('app.cart', 'app_cart_index'); $this->assertInstanceOf(Index::class, $operation); $this->assertSame('app_cart_index', $operation->getName()); $this->assertSame(['GET'], $operation->getMethods()); - $this->assertSame('app.repository.cart', $operation->getRepository()); $this->assertSame('App\Form\CartType', $operation->getFormType()); $operation = $metadataCollection->getOperation('app.cart', 'app_cart_show'); $this->assertInstanceOf(Show::class, $operation); $this->assertSame('app_cart_show', $operation->getName()); $this->assertSame(['GET'], $operation->getMethods()); - $this->assertSame('app.repository.cart', $operation->getRepository()); $this->assertSame('App\Form\CartType', $operation->getFormType()); } diff --git a/src/Component/tests/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactoryTest.php b/src/Component/tests/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactoryTest.php index ff76e9c57..6e777f28c 100644 --- a/src/Component/tests/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactoryTest.php +++ b/src/Component/tests/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactoryTest.php @@ -23,7 +23,6 @@ use Sylius\Resource\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use Sylius\Resource\Metadata\Resource\ResourceMetadataCollection; use Sylius\Resource\Metadata\ResourceMetadata; -use Sylius\Resource\Symfony\Request\State\Provider; final class ProviderResourceMetadataCollectionFactoryTest extends TestCase { @@ -44,27 +43,6 @@ public function testItIsInitializable(): void $this->assertInstanceOf(ProviderResourceMetadataCollectionFactory::class, $this->factory); } - public function testItCreatesResourceMetadataWithDefaultProviderOnHttpOperations(): void - { - $resource = new ResourceMetadata(alias: 'app.book', name: 'book', applicationName: 'app'); - - $index = (new Index(name: 'app_book_index'))->withResource($resource); - - $resource = $resource->withOperations(new Operations([ - $index->getName() => $index, - ])); - - $resourceMetadataCollection = new ResourceMetadataCollection(); - $resourceMetadataCollection[] = $resource; - - $this->decorated->create('App\Resource')->willReturn($resourceMetadataCollection); - - $resourceMetadataCollection = $this->factory->create('App\Resource'); - - $index = $resourceMetadataCollection->getOperation('app.book', 'app_book_index'); - $this->assertSame(Provider::class, $index->getProvider()); - } - public function testItConfiguresRequestGridProviderIfOperationHasAGrid(): void { $resource = new ResourceMetadata(alias: 'app.book', name: 'book', applicationName: 'app'); diff --git a/tests/Application/config/services.yaml b/tests/Application/config/services.yaml index e688256a7..b41a36298 100644 --- a/tests/Application/config/services.yaml +++ b/tests/Application/config/services.yaml @@ -99,9 +99,6 @@ services: App\Subscription\: resource: '../src/Subscription' - App\Subscription\Factory\SubscriptionFactory: - decorates: 'app.factory.subscription' - app.service.legacy_autowired_repository: class: App\Service\LegacyAutowiredRepositoryService autowire: true diff --git a/tests/Application/src/Subscription/Entity/Subscription.php b/tests/Application/src/Subscription/Entity/Subscription.php index 1e2aa6e50..e721ece8e 100644 --- a/tests/Application/src/Subscription/Entity/Subscription.php +++ b/tests/Application/src/Subscription/Entity/Subscription.php @@ -13,7 +13,9 @@ namespace App\Subscription\Entity; +use App\Subscription\Factory\SubscriptionFactory; use App\Subscription\Form\Type\SubscriptionType; +use App\Subscription\Repository\SubscriptionRepository; use App\Subscription\Twig\Context\Factory\ShowSubscriptionContextFactory; use Doctrine\ORM\Mapping as ORM; use Sylius\Resource\Metadata\Api; @@ -35,9 +37,10 @@ formType: SubscriptionType::class, templatesDir: 'crud', routePrefix: '/admin', + driver: false, )] #[Index(grid: 'app_subscription')] -#[Create] +#[Create(factory: [SubscriptionFactory::class, 'createNew'])] #[Update] #[Delete] #[BulkDelete] @@ -59,6 +62,7 @@ routePrefix: '/ajax', normalizationContext: ['groups' => 'subscription:read'], denormalizationContext: ['groups' => 'subscription:write'], + driver: false, )] #[Api\GetCollection] #[Api\Post] @@ -66,7 +70,7 @@ #[Api\Delete] #[Api\Get] -#[ORM\Entity] +#[ORM\Entity(repositoryClass: SubscriptionRepository::class)] class Subscription implements ResourceInterface { #[ORM\Column(type: 'string')] diff --git a/tests/Application/src/Subscription/Factory/SubscriptionFactory.php b/tests/Application/src/Subscription/Factory/SubscriptionFactory.php index 01c35ffb1..230c3a7c1 100644 --- a/tests/Application/src/Subscription/Factory/SubscriptionFactory.php +++ b/tests/Application/src/Subscription/Factory/SubscriptionFactory.php @@ -14,11 +14,10 @@ namespace App\Subscription\Factory; use App\Subscription\Entity\Subscription; -use Sylius\Resource\Factory\FactoryInterface; -final class SubscriptionFactory implements FactoryInterface +final class SubscriptionFactory { - public function createNew(): Subscription + public static function createNew(): Subscription { return new Subscription(email: 'new@example.com'); } diff --git a/tests/Application/src/Subscription/Repository/SubscriptionRepository.php b/tests/Application/src/Subscription/Repository/SubscriptionRepository.php new file mode 100644 index 000000000..ca5b9ed45 --- /dev/null +++ b/tests/Application/src/Subscription/Repository/SubscriptionRepository.php @@ -0,0 +1,29 @@ +assertResponseRedirects(null, expectedCode: Response::HTTP_FOUND); - /** @var Subscription $subscription */ - $subscription = static::getContainer()->get('app.repository.subscription')->findOneBy(['email' => 'biff.tannen@bttf.com']); + $subscription = $this->getSubscriptionRepository()->findOneBy(['email' => 'biff.tannen@bttf.com']); $this->assertNotNull($subscription); $this->assertSame('biff.tannen@bttf.com', (string) $subscription->email); @@ -180,8 +181,7 @@ public function it_allows_deleting_a_subscription(): void $this->assertResponseRedirects(null, expectedCode: Response::HTTP_FOUND); - /** @var Subscription[] $subscriptions */ - $subscriptions = static::getContainer()->get('app.repository.subscription')->findAll(); + $subscriptions = $this->getSubscriptionRepository()->findAll(); $this->assertEmpty($subscriptions); } @@ -196,8 +196,7 @@ public function it_allows_deleting_multiple_subscriptions(): void $this->assertResponseRedirects(null, expectedCode: Response::HTTP_FOUND); - /** @var Subscription[] $subscriptions */ - $subscriptions = static::getContainer()->get('app.repository.subscription')->findAll(); + $subscriptions = $this->getSubscriptionRepository()->findAll(); $this->assertEmpty($subscriptions); } @@ -240,4 +239,12 @@ protected function buildMatcher(): Matcher { return $this->matcherFactory->createMatcher(new VoidBacktrace()); } + + /** + * @return ObjectRepository + */ + private function getSubscriptionRepository(): ObjectRepository + { + return static::getContainer()->get(EntityManagerInterface::class)->getRepository(Subscription::class); + } } From f346d43533d2c22fbcd537926ae3294ae490253a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Wed, 19 Feb 2025 16:23:38 +0100 Subject: [PATCH 05/12] Apply suggestions from code review Co-authored-by: Dmitri Perunov --- .../Factory/DoctrineResourceMetadataCollectionFactory.php | 8 +++++++- .../DoctrineORMResourceMetadataCollectionFactory.php | 3 --- src/Component/src/Metadata/ResourceMetadata.php | 6 +++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Component/src/Doctrine/Common/Metadata/Resource/Factory/DoctrineResourceMetadataCollectionFactory.php b/src/Component/src/Doctrine/Common/Metadata/Resource/Factory/DoctrineResourceMetadataCollectionFactory.php index dce1e9865..f4ae8daac 100644 --- a/src/Component/src/Doctrine/Common/Metadata/Resource/Factory/DoctrineResourceMetadataCollectionFactory.php +++ b/src/Component/src/Doctrine/Common/Metadata/Resource/Factory/DoctrineResourceMetadataCollectionFactory.php @@ -30,7 +30,13 @@ public function __construct( private RegistryInterface $resourceRegistry, private ResourceMetadataCollectionFactoryInterface $decorated, ) { - trigger_deprecation('sylius/resource', '1.13', 'The "%s" is deprecated use "%s instead.', self::class, DoctrineORMResourceMetadataCollectionFactory::class); + trigger_deprecation( + 'sylius/resource', + '1.13', + 'The "%s" class is deprecated, use "%s instead. It will be removed in 2.0.', + self::class, + DoctrineORMResourceMetadataCollectionFactory::class, + ); } public function create(string $resourceClass): ResourceMetadataCollection diff --git a/src/Component/src/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactory.php b/src/Component/src/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactory.php index 6e76dbf9f..93293d7a3 100644 --- a/src/Component/src/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactory.php +++ b/src/Component/src/Doctrine/ORM/Metadata/Resource/Factory/DoctrineORMResourceMetadataCollectionFactory.php @@ -40,7 +40,6 @@ public function create(string $resourceClass): ResourceMetadataCollection /** @var ResourceMetadata $resource */ foreach ($resourceCollectionMetadata->getIterator() as $i => $resource) { $operations = $resource->getOperations() ?? new Operations(); - $entityClass = $resource->getClass(); if (null === $entityClass) { @@ -51,7 +50,6 @@ public function create(string $resourceClass): ResourceMetadataCollection foreach ($operations as $operation) { /** @var string $key */ $key = $operation->getName(); - $entityManager = $this->managerRegistry->getManagerForClass($entityClass); if (!$entityManager instanceof EntityManagerInterface) { @@ -64,7 +62,6 @@ public function create(string $resourceClass): ResourceMetadataCollection } $resource = $resource->withOperations($operations); - $resourceCollectionMetadata[$i] = $resource; } diff --git a/src/Component/src/Metadata/ResourceMetadata.php b/src/Component/src/Metadata/ResourceMetadata.php index d9ffcd479..404618c3f 100644 --- a/src/Component/src/Metadata/ResourceMetadata.php +++ b/src/Component/src/Metadata/ResourceMetadata.php @@ -41,7 +41,11 @@ public function __construct( $this->operations = null === $operations ? null : new Operations($operations); if (null !== $driver && false !== $driver) { - trigger_deprecation('sylius/resource', '1.13', 'Using driver is deprecated. If your resource is managed by Doctrine you have nothing to do, otherwise use a customer provider.'); + trigger_deprecation( + 'sylius/resource', + '1.13', + 'Using driver is deprecated. If your resource is managed by Doctrine you have nothing to do, otherwise use a custom provider.', + ); } } From 69d39f1e9dcc28b88c205f205c5a299119951bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Thu, 20 Feb 2025 08:19:20 +0100 Subject: [PATCH 06/12] Add Operation defaults trait --- .../Resources/config/services/metadata.xml | 12 +- .../Resources/config/services/routing.xml | 2 +- ...tNameResourceMetadataCollectionFactory.php | 8 +- ...ctoryResourceMetadataCollectionFactory.php | 8 +- .../Factory/OperationDefaultsTrait.php | 134 ++++++++++++++++++ ...viderResourceMetadataCollectionFactory.php | 8 +- ...chineResourceMetadataCollectionFactory.php | 8 +- ...esDirResourceMetadataCollectionFactory.php | 8 +- 8 files changed, 162 insertions(+), 26 deletions(-) create mode 100644 src/Component/src/Metadata/Resource/Factory/OperationDefaultsTrait.php diff --git a/src/Bundle/Resources/config/services/metadata.xml b/src/Bundle/Resources/config/services/metadata.xml index b6a0110fc..2de142149 100644 --- a/src/Bundle/Resources/config/services/metadata.xml +++ b/src/Bundle/Resources/config/services/metadata.xml @@ -21,16 +21,18 @@ - + - + @@ -78,7 +80,7 @@ %sylius.resource.settings% @@ -86,7 +88,7 @@ diff --git a/src/Bundle/Resources/config/services/routing.xml b/src/Bundle/Resources/config/services/routing.xml index 702dc90e3..7a7c17dd5 100644 --- a/src/Bundle/Resources/config/services/routing.xml +++ b/src/Bundle/Resources/config/services/routing.xml @@ -106,7 +106,7 @@ - + diff --git a/src/Component/src/Metadata/Resource/Factory/EventShortNameResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/EventShortNameResourceMetadataCollectionFactory.php index 7311c551d..23bf61d29 100644 --- a/src/Component/src/Metadata/Resource/Factory/EventShortNameResourceMetadataCollectionFactory.php +++ b/src/Component/src/Metadata/Resource/Factory/EventShortNameResourceMetadataCollectionFactory.php @@ -29,10 +29,10 @@ public function __construct( public function create(string $resourceClass): ResourceMetadataCollection { - $resourceCollectionMetadata = $this->decorated->create($resourceClass); + $resourceMetadataCollection = $this->decorated->create($resourceClass); /** @var ResourceMetadata $resource */ - foreach ($resourceCollectionMetadata->getIterator() as $i => $resource) { + foreach ($resourceMetadataCollection->getIterator() as $i => $resource) { $operations = $resource->getOperations() ?? new Operations(); /** @var Operation $operation */ @@ -45,10 +45,10 @@ public function create(string $resourceClass): ResourceMetadataCollection $resource = $resource->withOperations($operations); - $resourceCollectionMetadata[$i] = $resource; + $resourceMetadataCollection[$i] = $resource; } - return $resourceCollectionMetadata; + return $resourceMetadataCollection; } private function addDefaults(ResourceMetadata $resource, Operation $operation): Operation diff --git a/src/Component/src/Metadata/Resource/Factory/FactoryResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/FactoryResourceMetadataCollectionFactory.php index 26eb23c8e..bc6c10c4e 100644 --- a/src/Component/src/Metadata/Resource/Factory/FactoryResourceMetadataCollectionFactory.php +++ b/src/Component/src/Metadata/Resource/Factory/FactoryResourceMetadataCollectionFactory.php @@ -31,10 +31,10 @@ public function __construct( public function create(string $resourceClass): ResourceMetadataCollection { - $resourceCollectionMetadata = $this->decorated->create($resourceClass); + $resourceMetadataCollection = $this->decorated->create($resourceClass); /** @var ResourceMetadata $resource */ - foreach ($resourceCollectionMetadata->getIterator() as $i => $resource) { + foreach ($resourceMetadataCollection->getIterator() as $i => $resource) { $resourceConfiguration = $this->resourceRegistry->get($resource->getAlias() ?? ''); $operations = $resource->getOperations() ?? new Operations(); @@ -55,10 +55,10 @@ public function create(string $resourceClass): ResourceMetadataCollection $resource = $resource->withOperations($operations); - $resourceCollectionMetadata[$i] = $resource; + $resourceMetadataCollection[$i] = $resource; } - return $resourceCollectionMetadata; + return $resourceMetadataCollection; } private function addDefaults(MetadataInterface $resourceConfiguration, ResourceMetadata $resource, FactoryAwareOperationInterface $operation): FactoryAwareOperationInterface diff --git a/src/Component/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Component/src/Metadata/Resource/Factory/OperationDefaultsTrait.php new file mode 100644 index 000000000..3641f209b --- /dev/null +++ b/src/Component/src/Metadata/Resource/Factory/OperationDefaultsTrait.php @@ -0,0 +1,134 @@ +withClass($resourceClass); + + if (null === $resource->getAlias()) { + $resource = $resource->withAlias($resourceConfiguration->getAlias()); + } + + if (null === $resource->getApplicationName()) { + $resource = $resource->withApplicationName($resourceConfiguration->getApplicationName()); + } + + if (null === $resource->getName()) { + $resource = $resource->withName($resourceConfiguration->getName()); + } + + return $resource; + } + + private function getOperationWithDefaults(OperationRouteNameFactory $operationRouteNameFactory, RegistryInterface $resourceRegistry, ResourceMetadata $resource, Operation $operation): array + { + $resourceConfiguration = $resourceRegistry->get($resource->getAlias() ?? ''); + + $operation = $operation->withResource($resource); + + if (null === $resource->getName()) { + $resourceName = $resourceConfiguration->getName(); + + $resource = $resource->withName($resourceName); + $operation = $operation->withResource($resource); + } + + if (null === $resource->getPluralName()) { + $resourcePluralName = $resourceConfiguration->getPluralName(); + + $resource = $resource->withPluralName($resourcePluralName); + $operation = $operation->withResource($resource); + } + + if (null === $operation->getNormalizationContext()) { + $operation = $operation->withNormalizationContext($resource->getNormalizationContext()); + } + + if (null === $operation->getDenormalizationContext()) { + $operation = $operation->withDenormalizationContext($resource->getDenormalizationContext()); + } + + if (null === $operation->getValidationContext()) { + $operation = $operation->withValidationContext($resource->getValidationContext()); + } + + $operation = $operation->withResource($resource); + + if (null === $operation->getFormType()) { + $formType = $resource->getFormType() ?? $resourceConfiguration->getClass('form'); + $operation = $operation->withFormType($formType); + } + + $formOptions = $this->buildFormOptions($operation, $resourceConfiguration); + $operation = $operation->withFormOptions($formOptions); + + if ($operation instanceof HttpOperation) { + if (null === $operation->getRoutePrefix()) { + $operation = $operation->withRoutePrefix($resource->getRoutePrefix() ?? null); + } + + if (null === $operation->getTwigContextFactory()) { + $operation = $operation->withTwigContextFactory('sylius.twig.context.factory.default'); + } + + if (null === $routeName = $operation->getRouteName()) { + $routeName = $operationRouteNameFactory->createRouteName($operation); + $operation = $operation->withRouteName($routeName); + } + + if (null === $operation->getResponder()) { + $operation = $operation->withResponder(Responder::class); + } + + $operation = $operation->withName($routeName); + } + + $operationName = $operation->getName(); + + return [$operationName, $operation]; + } + + private function buildFormOptions(Operation $operation, MetadataInterface $resourceConfiguration): array + { + $formOptions = array_merge( + ['data_class' => $resourceConfiguration->getClass('model')], + $operation->getFormOptions() ?? [], + ); + + $validationGroups = $operation->getValidationContext()['groups'] ?? null; + + if (null !== $validationGroups) { + $formOptions = array_merge(['validation_groups' => $validationGroups], $formOptions); + } + + return $formOptions; + } +} diff --git a/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php index 27ff1ec3f..13045c05f 100644 --- a/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php +++ b/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php @@ -29,10 +29,10 @@ public function __construct( public function create(string $resourceClass): ResourceMetadataCollection { - $resourceCollectionMetadata = $this->decorated->create($resourceClass); + $resourceMetadataCollection = $this->decorated->create($resourceClass); /** @var ResourceMetadata $resource */ - foreach ($resourceCollectionMetadata->getIterator() as $i => $resource) { + foreach ($resourceMetadataCollection->getIterator() as $i => $resource) { $operations = $resource->getOperations() ?? new Operations(); /** @var Operation $operation */ @@ -45,10 +45,10 @@ public function create(string $resourceClass): ResourceMetadataCollection $resource = $resource->withOperations($operations); - $resourceCollectionMetadata[$i] = $resource; + $resourceMetadataCollection[$i] = $resource; } - return $resourceCollectionMetadata; + return $resourceMetadataCollection; } private function addDefaults(Operation $operation): Operation diff --git a/src/Component/src/Metadata/Resource/Factory/StateMachineResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/StateMachineResourceMetadataCollectionFactory.php index 7f92bd070..daeb583d6 100644 --- a/src/Component/src/Metadata/Resource/Factory/StateMachineResourceMetadataCollectionFactory.php +++ b/src/Component/src/Metadata/Resource/Factory/StateMachineResourceMetadataCollectionFactory.php @@ -33,10 +33,10 @@ public function __construct( public function create(string $resourceClass): ResourceMetadataCollection { - $resourceCollectionMetadata = $this->decorated->create($resourceClass); + $resourceMetadataCollection = $this->decorated->create($resourceClass); /** @var ResourceMetadata $resource */ - foreach ($resourceCollectionMetadata->getIterator() as $i => $resource) { + foreach ($resourceMetadataCollection->getIterator() as $i => $resource) { $resourceConfiguration = $this->resourceRegistry->get($resource->getAlias() ?? ''); $operations = $resource->getOperations() ?? new Operations(); @@ -50,10 +50,10 @@ public function create(string $resourceClass): ResourceMetadataCollection $resource = $resource->withOperations($operations); - $resourceCollectionMetadata[$i] = $resource; + $resourceMetadataCollection[$i] = $resource; } - return $resourceCollectionMetadata; + return $resourceMetadataCollection; } private function addDefaults(MetadataInterface $resourceConfiguration, Operation $operation): Operation diff --git a/src/Component/src/Metadata/Resource/Factory/TemplatesDirResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/TemplatesDirResourceMetadataCollectionFactory.php index 7e1608cda..d112e3f58 100644 --- a/src/Component/src/Metadata/Resource/Factory/TemplatesDirResourceMetadataCollectionFactory.php +++ b/src/Component/src/Metadata/Resource/Factory/TemplatesDirResourceMetadataCollectionFactory.php @@ -28,10 +28,10 @@ public function __construct( public function create(string $resourceClass): ResourceMetadataCollection { - $resourceCollectionMetadata = $this->decorated->create($resourceClass); + $resourceMetadataCollection = $this->decorated->create($resourceClass); /** @var ResourceMetadata $resource */ - foreach ($resourceCollectionMetadata->getIterator() as $i => $resource) { + foreach ($resourceMetadataCollection->getIterator() as $i => $resource) { $operations = $resource->getOperations() ?? new Operations(); /** @var Operation $operation */ @@ -44,10 +44,10 @@ public function create(string $resourceClass): ResourceMetadataCollection $resource = $resource->withOperations($operations); - $resourceCollectionMetadata[$i] = $resource; + $resourceMetadataCollection[$i] = $resource; } - return $resourceCollectionMetadata; + return $resourceMetadataCollection; } private function addDefaults(ResourceMetadata $resource, Operation $operation): Operation From d90f4ee2b0377007a12bb3675f514d2b50f0867e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Fri, 21 Feb 2025 14:42:35 +0100 Subject: [PATCH 07/12] PoC PHP file resource metadata factory --- psalm.xml | 18 ++ .../DependencyInjection/Configuration.php | 3 + .../Resources/config/services/metadata.xml | 19 ++- .../services/metadata/resource_name.xml | 10 ++ .../Resources/config/services/state.xml | 2 +- .../AttributesOperationRouteFactorySpec.php | 1 - .../Extractor/MetadataExtractorInterface.php | 24 +++ .../Extractor/PhpFileMetadataExtractor.php | 158 ++++++++++++++++++ ...butesResourceMetadataCollectionFactory.php | 4 + ...pFileResourceMetadataCollectionFactory.php | 73 ++++++++ .../PhpFileResourceNameCollectionFactory.php | 51 ++++++ .../Application/config/packages/doctrine.yaml | 5 + .../Application/config/sylius/resources.yaml | 6 + .../config/sylius/resources/speaker.php | 29 ++++ .../src/Conference/Entity/Speaker.php | 41 +++++ .../Configuration/ConfigurationTest.php | 2 + .../SyliusResourceExtensionTest.php | 1 + 17 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 src/Component/src/Metadata/Extractor/MetadataExtractorInterface.php create mode 100644 src/Component/src/Metadata/Extractor/PhpFileMetadataExtractor.php create mode 100644 src/Component/src/Metadata/Resource/Factory/PhpFileResourceMetadataCollectionFactory.php create mode 100644 src/Component/src/Metadata/Resource/Factory/PhpFileResourceNameCollectionFactory.php create mode 100644 tests/Application/config/sylius/resources/speaker.php create mode 100644 tests/Application/src/Conference/Entity/Speaker.php diff --git a/psalm.xml b/psalm.xml index 81de9bc5f..026a57b15 100644 --- a/psalm.xml +++ b/psalm.xml @@ -116,6 +116,12 @@ + + + + + + @@ -179,6 +185,12 @@ + + + + + + @@ -320,6 +332,12 @@ + + + + + + diff --git a/src/Bundle/DependencyInjection/Configuration.php b/src/Bundle/DependencyInjection/Configuration.php index 6de235407..7da7477d5 100644 --- a/src/Bundle/DependencyInjection/Configuration.php +++ b/src/Bundle/DependencyInjection/Configuration.php @@ -39,6 +39,9 @@ public function getConfigTreeBuilder(): TreeBuilder ->arrayNode('mapping') ->addDefaultsIfNotSet() ->children() + ->arrayNode('imports') + ->prototype('scalar')->end() + ->end() ->arrayNode('paths') ->prototype('scalar')->end() ->end() diff --git a/src/Bundle/Resources/config/services/metadata.xml b/src/Bundle/Resources/config/services/metadata.xml index 2de142149..37ca16ea2 100644 --- a/src/Bundle/Resources/config/services/metadata.xml +++ b/src/Bundle/Resources/config/services/metadata.xml @@ -21,14 +21,29 @@ + + %sylius.resource.mapping% + + + + + + + + + + + + + - - %sylius.resource.mapping% + + + + + + diff --git a/src/Bundle/Resources/config/services/state.xml b/src/Bundle/Resources/config/services/state.xml index 11b9cf977..597454bc2 100644 --- a/src/Bundle/Resources/config/services/state.xml +++ b/src/Bundle/Resources/config/services/state.xml @@ -64,7 +64,7 @@ - + diff --git a/src/Component/spec/Symfony/Routing/Factory/AttributesOperationRouteFactorySpec.php b/src/Component/spec/Symfony/Routing/Factory/AttributesOperationRouteFactorySpec.php index a55bc8421..2983df629 100644 --- a/src/Component/spec/Symfony/Routing/Factory/AttributesOperationRouteFactorySpec.php +++ b/src/Component/spec/Symfony/Routing/Factory/AttributesOperationRouteFactorySpec.php @@ -42,7 +42,6 @@ function let( new AttributesResourceMetadataCollectionFactory( $resourceRegistry->getWrappedObject(), new OperationRouteNameFactory(), - 'symfony', ), ); } diff --git a/src/Component/src/Metadata/Extractor/MetadataExtractorInterface.php b/src/Component/src/Metadata/Extractor/MetadataExtractorInterface.php new file mode 100644 index 000000000..4c8dd48b2 --- /dev/null +++ b/src/Component/src/Metadata/Extractor/MetadataExtractorInterface.php @@ -0,0 +1,24 @@ +getResourceFilePaths() as $filePath) { + if (!is_readable($filePath)) { + continue; + } + + $resource = $this->getPHPFileClosure($filePath)(); + + if (!$resource instanceof ResourceMetadata) { + continue; + } + + $resourceReflection = new \ReflectionClass($resource); + + foreach ($resourceReflection->getProperties() as $property) { + $property->setAccessible(true); + $resolvedValue = $this->resolve($property->getValue($resource)); + $property->setValue($resource, $resolvedValue); + } + + $metadata[] = $resource; + } + + return $metadata; + } + + private function getResourceFilePaths(): iterable + { + foreach ($this->createFinder() as $file) { + yield $file->getPathname(); + } + } + + private function createFinder(): Finder + { + $finder = (new Finder())->files(); + + foreach ($this->resourceMapping['imports'] ?? [] as $path) { + $finder->in($path); + } + + return $finder->files(); + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ + private function getPHPFileClosure(string $filePath): \Closure + { + return \Closure::bind(function () use ($filePath): mixed { + return require $filePath; + }, null, null); + } + + /** + * Recursively replaces placeholders with the service container parameters. + * + * @see https://github.com/symfony/symfony/blob/6fec32c/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php + * + * @param mixed $value The source which might contain "%placeholders%" + * + * @throws \RuntimeException When a container value is not a string or a numeric value + * + * @return mixed The source with the placeholders replaced by the container + * parameters. Arrays are resolved recursively. + */ + private function resolve(mixed $value): mixed + { + $container = $this->container; + + if (null === $container) { + return $value; + } + + if (\is_array($value)) { + foreach ($value as $key => $val) { + $value[$key] = $this->resolve($val); + } + + return $value; + } + + if (!\is_string($value)) { + return $value; + } + + $escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($value, $container) { + $parameter = $match[1] ?? null; + + // skip %% + if (!isset($parameter)) { + return '%%'; + } + + if (preg_match('/^env\(\w+\)$/', $parameter)) { + throw new \RuntimeException(\sprintf('Using "%%%s%%" is not allowed in routing configuration.', $parameter)); + } + + if (\array_key_exists($parameter, $this->collectedParameters)) { + return $this->collectedParameters[$parameter]; + } + + if ($container instanceof SymfonyContainerInterface) { + $resolved = $container->getParameter($parameter); + } else { + $resolved = $container->get($parameter); + } + + if (\is_string($resolved) || is_numeric($resolved)) { + $this->collectedParameters[$parameter] = $resolved; + + return (string) $resolved; + } + + throw new \RuntimeException(\sprintf('The container parameter "%s", used in the resource configuration value "%s", must be a string or numeric, but it is of type %s.', $parameter, $value, \gettype($resolved))); + }, $value); + + return str_replace('%%', '%', $escapedValue ?? ''); + } +} diff --git a/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index 0ae0097c1..e6b8b4df0 100644 --- a/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -30,12 +30,16 @@ final class AttributesResourceMetadataCollectionFactory implements ResourceMetad public function __construct( private RegistryInterface $resourceRegistry, private OperationRouteNameFactory $operationRouteNameFactory, + private ?ResourceMetadataCollectionFactoryInterface $decorated = null, ) { } public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = new ResourceMetadataCollection(); + if ($this->decorated) { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + } $attributes = ClassReflection::getClassAttributes($resourceClass); diff --git a/src/Component/src/Metadata/Resource/Factory/PhpFileResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/PhpFileResourceMetadataCollectionFactory.php new file mode 100644 index 000000000..97acb87f7 --- /dev/null +++ b/src/Component/src/Metadata/Resource/Factory/PhpFileResourceMetadataCollectionFactory.php @@ -0,0 +1,73 @@ +decorated) { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + } + + foreach ($this->metadataExtractor->extract() as $resource) { + if ($resourceClass !== $resource->getClass()) { + continue; + } + + $resourceAlias = $resource->getAlias(); + + if (null !== $resourceAlias) { + $resourceConfiguration = $this->resourceRegistry->get($resource->getAlias() ?? ''); + } else { + $resourceConfiguration = $this->resourceRegistry->getByClass($resourceClass); + } + + $resource = $this->getResourceWithDefaults($resourceClass, $resource, $resourceConfiguration); + + $operations = []; + /** @var Operation $operation */ + foreach ($resource->getOperations() ?? new Operations() as $operation) { + [$key, $operation] = $this->getOperationWithDefaults($this->operationRouteNameFactory, $this->resourceRegistry, $resource, $operation); + $operations[$key] = $operation; + } + + if ($operations) { + $resource = $resource->withOperations(new Operations($operations)); + } + + $resourceMetadataCollection[] = $resource; + } + + return $resourceMetadataCollection; + } +} diff --git a/src/Component/src/Metadata/Resource/Factory/PhpFileResourceNameCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/PhpFileResourceNameCollectionFactory.php new file mode 100644 index 000000000..401365050 --- /dev/null +++ b/src/Component/src/Metadata/Resource/Factory/PhpFileResourceNameCollectionFactory.php @@ -0,0 +1,51 @@ +decorated) { + foreach ($this->decorated->create() as $resourceClass) { + $classes[$resourceClass] = true; + } + } + + foreach ($this->metadataExtractor->extract() as $resource) { + $resourceClass = $resource->getClass(); + + if (null === $resourceClass) { + continue; + } + + $classes[$resourceClass] = true; + } + + return new ResourceNameCollection(array_keys($classes)); + } +} diff --git a/tests/Application/config/packages/doctrine.yaml b/tests/Application/config/packages/doctrine.yaml index 5c208b673..1ab04b5e0 100644 --- a/tests/Application/config/packages/doctrine.yaml +++ b/tests/Application/config/packages/doctrine.yaml @@ -19,6 +19,11 @@ doctrine: type: attribute dir: '%kernel.project_dir%/src/BoardGameBlog/Domain' prefix: 'App\BoardGameBlog\Domain' + Conference: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/src/Conference/Entity' + prefix: 'App\Conference\Entity' Subscription: is_bundle: false type: attribute diff --git a/tests/Application/config/sylius/resources.yaml b/tests/Application/config/sylius/resources.yaml index e4f291993..08b41c935 100644 --- a/tests/Application/config/sylius/resources.yaml +++ b/tests/Application/config/sylius/resources.yaml @@ -1,5 +1,7 @@ sylius_resource: mapping: + imports: + - '%kernel.project_dir%/config/sylius/resources' paths: - '%kernel.project_dir%/src/BoardGameBlog/Infrastructure/Sylius/Resource' - '%kernel.project_dir%/src/Subscription/Entity' @@ -61,3 +63,7 @@ sylius_resource: classes: model: App\Entity\Zone\ZoneMember interface: App\Entity\Zone\ZoneMemberInterface + + app.speaker: + classes: + model: App\Conference\Entity\Speaker diff --git a/tests/Application/config/sylius/resources/speaker.php b/tests/Application/config/sylius/resources/speaker.php new file mode 100644 index 000000000..660c1316d --- /dev/null +++ b/tests/Application/config/sylius/resources/speaker.php @@ -0,0 +1,29 @@ +withRoutePrefix('/admin') + ->withClass(Speaker::class) + ->withSection('admin') + ->withTemplatesDir('crud') + ->withOperations(new Operations([ + new Create(), + new Index(), + ])) +; diff --git a/tests/Application/src/Conference/Entity/Speaker.php b/tests/Application/src/Conference/Entity/Speaker.php new file mode 100644 index 000000000..3be181e85 --- /dev/null +++ b/tests/Application/src/Conference/Entity/Speaker.php @@ -0,0 +1,41 @@ +id; + } +} diff --git a/tests/Bundle/Configuration/ConfigurationTest.php b/tests/Bundle/Configuration/ConfigurationTest.php index 54865c590..0ba04f7ed 100644 --- a/tests/Bundle/Configuration/ConfigurationTest.php +++ b/tests/Bundle/Configuration/ConfigurationTest.php @@ -72,6 +72,7 @@ public function it_has_no_default_mapping_paths(): void [ 'mapping' => [ 'paths' => [], + 'imports' => [], ], ], 'mapping', @@ -92,6 +93,7 @@ public function its_mapping_paths_can_be_customized(): void 'paths' => [ 'path/to/resources', ], + 'imports' => [], ], ], 'mapping', diff --git a/tests/Bundle/DependencyInjection/SyliusResourceExtensionTest.php b/tests/Bundle/DependencyInjection/SyliusResourceExtensionTest.php index 11a4e2c50..021631cc4 100644 --- a/tests/Bundle/DependencyInjection/SyliusResourceExtensionTest.php +++ b/tests/Bundle/DependencyInjection/SyliusResourceExtensionTest.php @@ -126,6 +126,7 @@ public function it_registers_parameter_for_paths(): void 'paths' => [ __DIR__ . '/Dummy', ], + 'imports' => [], ]); } From adc137c8364076c7a4db846853b52ee244a97fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Thu, 27 Feb 2025 12:10:50 +0100 Subject: [PATCH 08/12] Add route requirements --- .../Factory/OperationRouteFactorySpec.php | 18 ++++++++++++++++++ src/Component/src/Metadata/Api/Delete.php | 2 ++ src/Component/src/Metadata/Api/Get.php | 2 ++ .../src/Metadata/Api/GetCollection.php | 2 ++ src/Component/src/Metadata/Api/Patch.php | 2 ++ src/Component/src/Metadata/Api/Post.php | 2 ++ src/Component/src/Metadata/Api/Put.php | 2 ++ src/Component/src/Metadata/BulkDelete.php | 2 ++ src/Component/src/Metadata/BulkUpdate.php | 2 ++ src/Component/src/Metadata/Create.php | 2 ++ src/Component/src/Metadata/Delete.php | 2 ++ src/Component/src/Metadata/HttpOperation.php | 14 ++++++++++++++ src/Component/src/Metadata/Index.php | 2 ++ src/Component/src/Metadata/Show.php | 2 ++ src/Component/src/Metadata/Update.php | 2 ++ .../Routing/Factory/OperationRouteFactory.php | 1 + .../Command/DebugResourceCommandTest.php | 1 + 17 files changed, 60 insertions(+) diff --git a/src/Component/spec/Symfony/Routing/Factory/OperationRouteFactorySpec.php b/src/Component/spec/Symfony/Routing/Factory/OperationRouteFactorySpec.php index edc46a93f..b03514be2 100644 --- a/src/Component/spec/Symfony/Routing/Factory/OperationRouteFactorySpec.php +++ b/src/Component/spec/Symfony/Routing/Factory/OperationRouteFactorySpec.php @@ -289,4 +289,22 @@ function it_generates_routes_with_vars( ], ]); } + + function it_generates_routes_with_requirements( + OperationRoutePathFactoryInterface $routePathFactory, + ): void { + $operation = new Index(routeRequirements: ['country', 'province', 'zone']); + + $metadata = Metadata::fromAliasAndConfiguration('app.dummy', ['driver' => 'dummy_driver']); + + $routePathFactory->createRoutePath($operation, 'dummies')->willReturn('/dummies')->shouldBeCalled(); + + $route = $this->create( + $metadata, + new ResourceMetadata(alias: 'app.dummy'), + $operation, + ); + + $route->getRequirements()->shouldReturn(['country', 'province', 'zone']); + } } diff --git a/src/Component/src/Metadata/Api/Delete.php b/src/Component/src/Metadata/Api/Delete.php index edc92537f..86d17b472 100644 --- a/src/Component/src/Metadata/Api/Delete.php +++ b/src/Component/src/Metadata/Api/Delete.php @@ -25,6 +25,7 @@ final class Delete extends HttpOperation implements DeleteOperationInterface, Ap public function __construct( ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -51,6 +52,7 @@ public function __construct( methods: ['DELETE'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'delete', name: $name, diff --git a/src/Component/src/Metadata/Api/Get.php b/src/Component/src/Metadata/Api/Get.php index ae77d6303..426d1a059 100644 --- a/src/Component/src/Metadata/Api/Get.php +++ b/src/Component/src/Metadata/Api/Get.php @@ -25,6 +25,7 @@ final class Get extends HttpOperation implements ShowOperationInterface, ApiOper public function __construct( ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -51,6 +52,7 @@ public function __construct( methods: ['GET'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'get', name: $name, diff --git a/src/Component/src/Metadata/Api/GetCollection.php b/src/Component/src/Metadata/Api/GetCollection.php index b4cbe0ad1..b7fde43f2 100644 --- a/src/Component/src/Metadata/Api/GetCollection.php +++ b/src/Component/src/Metadata/Api/GetCollection.php @@ -25,6 +25,7 @@ final class GetCollection extends HttpOperation implements CollectionOperationIn public function __construct( ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -51,6 +52,7 @@ public function __construct( methods: ['GET'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'get_collection', name: $name, diff --git a/src/Component/src/Metadata/Api/Patch.php b/src/Component/src/Metadata/Api/Patch.php index 803418410..c0070426b 100644 --- a/src/Component/src/Metadata/Api/Patch.php +++ b/src/Component/src/Metadata/Api/Patch.php @@ -25,6 +25,7 @@ final class Patch extends HttpOperation implements UpdateOperationInterface, Api public function __construct( ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -51,6 +52,7 @@ public function __construct( methods: ['PATCH'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'patch', name: $name, diff --git a/src/Component/src/Metadata/Api/Post.php b/src/Component/src/Metadata/Api/Post.php index 4a0e96eb3..1eb156fd6 100644 --- a/src/Component/src/Metadata/Api/Post.php +++ b/src/Component/src/Metadata/Api/Post.php @@ -25,6 +25,7 @@ final class Post extends HttpOperation implements CreateOperationInterface, ApiO public function __construct( ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -51,6 +52,7 @@ public function __construct( methods: ['POST'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'post', name: $name, diff --git a/src/Component/src/Metadata/Api/Put.php b/src/Component/src/Metadata/Api/Put.php index d4a954679..ff438a3a6 100644 --- a/src/Component/src/Metadata/Api/Put.php +++ b/src/Component/src/Metadata/Api/Put.php @@ -25,6 +25,7 @@ final class Put extends HttpOperation implements UpdateOperationInterface, ApiOp public function __construct( ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -51,6 +52,7 @@ public function __construct( methods: ['PUT'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'put', name: $name, diff --git a/src/Component/src/Metadata/BulkDelete.php b/src/Component/src/Metadata/BulkDelete.php index b0798d492..dee30e651 100644 --- a/src/Component/src/Metadata/BulkDelete.php +++ b/src/Component/src/Metadata/BulkDelete.php @@ -23,6 +23,7 @@ public function __construct( ?array $methods = null, ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -45,6 +46,7 @@ public function __construct( methods: $methods ?? ['DELETE', 'POST'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'bulk_delete', name: $name, diff --git a/src/Component/src/Metadata/BulkUpdate.php b/src/Component/src/Metadata/BulkUpdate.php index 9b5c1f9b9..fa3d30887 100644 --- a/src/Component/src/Metadata/BulkUpdate.php +++ b/src/Component/src/Metadata/BulkUpdate.php @@ -23,6 +23,7 @@ public function __construct( ?array $methods = null, ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -52,6 +53,7 @@ public function __construct( methods: $methods ?? ['PUT', 'PATCH'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'bulk_update', name: $name, diff --git a/src/Component/src/Metadata/Create.php b/src/Component/src/Metadata/Create.php index cf43df572..9f121ff3b 100644 --- a/src/Component/src/Metadata/Create.php +++ b/src/Component/src/Metadata/Create.php @@ -26,6 +26,7 @@ public function __construct( ?array $methods = null, ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -59,6 +60,7 @@ public function __construct( methods: $methods ?? ['GET', 'POST'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'create', name: $name, diff --git a/src/Component/src/Metadata/Delete.php b/src/Component/src/Metadata/Delete.php index 21eced536..302ad4f0a 100644 --- a/src/Component/src/Metadata/Delete.php +++ b/src/Component/src/Metadata/Delete.php @@ -23,6 +23,7 @@ public function __construct( ?array $methods = null, ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -50,6 +51,7 @@ public function __construct( methods: $methods ?? ['DELETE', 'POST'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'delete', name: $name, diff --git a/src/Component/src/Metadata/HttpOperation.php b/src/Component/src/Metadata/HttpOperation.php index 194166b67..a2dab3c52 100644 --- a/src/Component/src/Metadata/HttpOperation.php +++ b/src/Component/src/Metadata/HttpOperation.php @@ -26,6 +26,7 @@ public function __construct( protected ?string $path = null, protected ?string $routeName = null, protected ?string $routePrefix = null, + protected ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -129,6 +130,19 @@ public function withRoutePrefix(?string $routePrefix): self return $self; } + public function getRouteRequirements(): ?array + { + return $this->routeRequirements; + } + + public function withRouteRequirements(array $routeRequirements): self + { + $self = clone $this; + $self->routeRequirements = $routeRequirements; + + return $self; + } + public function getTwigContextFactory(): callable|string|null { return $this->twigContextFactory; diff --git a/src/Component/src/Metadata/Index.php b/src/Component/src/Metadata/Index.php index ae64fe889..10790bbbf 100644 --- a/src/Component/src/Metadata/Index.php +++ b/src/Component/src/Metadata/Index.php @@ -23,6 +23,7 @@ public function __construct( ?array $methods = null, ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -50,6 +51,7 @@ public function __construct( methods: $methods ?? ['GET'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'index', name: $name, diff --git a/src/Component/src/Metadata/Show.php b/src/Component/src/Metadata/Show.php index 72bbe12e2..8a8eeb74f 100644 --- a/src/Component/src/Metadata/Show.php +++ b/src/Component/src/Metadata/Show.php @@ -23,6 +23,7 @@ public function __construct( ?array $methods = null, ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -49,6 +50,7 @@ public function __construct( methods: $methods ?? ['GET'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'show', name: $name, diff --git a/src/Component/src/Metadata/Update.php b/src/Component/src/Metadata/Update.php index 85e393690..a62bab59e 100644 --- a/src/Component/src/Metadata/Update.php +++ b/src/Component/src/Metadata/Update.php @@ -23,6 +23,7 @@ public function __construct( ?array $methods = null, ?string $path = null, ?string $routePrefix = null, + ?array $routeRequirements = null, ?string $template = null, ?string $shortName = null, ?string $name = null, @@ -53,6 +54,7 @@ public function __construct( methods: $methods ?? ['GET', 'PUT', 'POST'], path: $path, routePrefix: $routePrefix, + routeRequirements: $routeRequirements, template: $template, shortName: $shortName ?? 'update', name: $name, diff --git a/src/Component/src/Symfony/Routing/Factory/OperationRouteFactory.php b/src/Component/src/Symfony/Routing/Factory/OperationRouteFactory.php index 88c09cd4a..15b790574 100644 --- a/src/Component/src/Symfony/Routing/Factory/OperationRouteFactory.php +++ b/src/Component/src/Symfony/Routing/Factory/OperationRouteFactory.php @@ -44,6 +44,7 @@ public function create(MetadataInterface $metadata, ResourceMetadata $resource, '_controller' => 'sylius.main_controller', '_sylius' => $this->getSyliusOptions($resource, $operation), ], + requirements: $operation->getRouteRequirements() ?? [], methods: $operation->getMethods() ?? [], ); } diff --git a/tests/Bundle/Command/DebugResourceCommandTest.php b/tests/Bundle/Command/DebugResourceCommandTest.php index 618c684c4..1d46f2b4e 100644 --- a/tests/Bundle/Command/DebugResourceCommandTest.php +++ b/tests/Bundle/Command/DebugResourceCommandTest.php @@ -283,6 +283,7 @@ public function it_displays_the_metadata_for_given_resource_operation(): void path null routeName null routePrefix null + routeRequirements null redirectToRoute null redirectArguments null vars [ From 3992255b7f79e50fa2b207f1c9dea46d49fce3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Thu, 27 Feb 2025 14:08:54 +0100 Subject: [PATCH 09/12] Apply suggestions from code review --- .../Symfony/Routing/Factory/OperationRouteFactorySpec.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Component/spec/Symfony/Routing/Factory/OperationRouteFactorySpec.php b/src/Component/spec/Symfony/Routing/Factory/OperationRouteFactorySpec.php index b03514be2..4c2c09640 100644 --- a/src/Component/spec/Symfony/Routing/Factory/OperationRouteFactorySpec.php +++ b/src/Component/spec/Symfony/Routing/Factory/OperationRouteFactorySpec.php @@ -293,7 +293,7 @@ function it_generates_routes_with_vars( function it_generates_routes_with_requirements( OperationRoutePathFactoryInterface $routePathFactory, ): void { - $operation = new Index(routeRequirements: ['country', 'province', 'zone']); + $operation = new Index(routeRequirements: ['type' => 'country|province|zone']); $metadata = Metadata::fromAliasAndConfiguration('app.dummy', ['driver' => 'dummy_driver']); @@ -305,6 +305,6 @@ function it_generates_routes_with_requirements( $operation, ); - $route->getRequirements()->shouldReturn(['country', 'province', 'zone']); + $route->getRequirements()->shouldReturn(['type' => 'country|province|zone']); } } From a0dd5eb81e366988630fe2e31cd56d8f3b1a91cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Fri, 28 Feb 2025 08:48:42 +0100 Subject: [PATCH 10/12] Fix missing route name on operations attributes --- src/Component/src/Metadata/Api/Delete.php | 2 ++ src/Component/src/Metadata/Api/Get.php | 2 ++ src/Component/src/Metadata/Api/GetCollection.php | 2 ++ src/Component/src/Metadata/Api/Patch.php | 2 ++ src/Component/src/Metadata/Api/Post.php | 2 ++ src/Component/src/Metadata/Api/Put.php | 2 ++ src/Component/src/Metadata/ApplyStateMachineTransition.php | 2 ++ src/Component/src/Metadata/BulkDelete.php | 2 ++ src/Component/src/Metadata/BulkUpdate.php | 2 ++ src/Component/src/Metadata/Create.php | 2 ++ src/Component/src/Metadata/Delete.php | 2 ++ src/Component/src/Metadata/Index.php | 2 ++ src/Component/src/Metadata/Show.php | 2 ++ src/Component/src/Metadata/Update.php | 2 ++ 14 files changed, 28 insertions(+) diff --git a/src/Component/src/Metadata/Api/Delete.php b/src/Component/src/Metadata/Api/Delete.php index 86d17b472..ca5b21226 100644 --- a/src/Component/src/Metadata/Api/Delete.php +++ b/src/Component/src/Metadata/Api/Delete.php @@ -24,6 +24,7 @@ final class Delete extends HttpOperation implements DeleteOperationInterface, Ap { public function __construct( ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -51,6 +52,7 @@ public function __construct( parent::__construct( methods: ['DELETE'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/Api/Get.php b/src/Component/src/Metadata/Api/Get.php index 426d1a059..db36d934f 100644 --- a/src/Component/src/Metadata/Api/Get.php +++ b/src/Component/src/Metadata/Api/Get.php @@ -24,6 +24,7 @@ final class Get extends HttpOperation implements ShowOperationInterface, ApiOper { public function __construct( ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -51,6 +52,7 @@ public function __construct( parent::__construct( methods: ['GET'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/Api/GetCollection.php b/src/Component/src/Metadata/Api/GetCollection.php index b7fde43f2..af38a475f 100644 --- a/src/Component/src/Metadata/Api/GetCollection.php +++ b/src/Component/src/Metadata/Api/GetCollection.php @@ -24,6 +24,7 @@ final class GetCollection extends HttpOperation implements CollectionOperationIn { public function __construct( ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -51,6 +52,7 @@ public function __construct( parent::__construct( methods: ['GET'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/Api/Patch.php b/src/Component/src/Metadata/Api/Patch.php index c0070426b..eb5349da0 100644 --- a/src/Component/src/Metadata/Api/Patch.php +++ b/src/Component/src/Metadata/Api/Patch.php @@ -24,6 +24,7 @@ final class Patch extends HttpOperation implements UpdateOperationInterface, Api { public function __construct( ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -51,6 +52,7 @@ public function __construct( parent::__construct( methods: ['PATCH'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/Api/Post.php b/src/Component/src/Metadata/Api/Post.php index 1eb156fd6..4aeb18a4c 100644 --- a/src/Component/src/Metadata/Api/Post.php +++ b/src/Component/src/Metadata/Api/Post.php @@ -24,6 +24,7 @@ final class Post extends HttpOperation implements CreateOperationInterface, ApiO { public function __construct( ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -51,6 +52,7 @@ public function __construct( parent::__construct( methods: ['POST'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/Api/Put.php b/src/Component/src/Metadata/Api/Put.php index ff438a3a6..1a22fc883 100644 --- a/src/Component/src/Metadata/Api/Put.php +++ b/src/Component/src/Metadata/Api/Put.php @@ -24,6 +24,7 @@ final class Put extends HttpOperation implements UpdateOperationInterface, ApiOp { public function __construct( ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -51,6 +52,7 @@ public function __construct( parent::__construct( methods: ['PUT'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/ApplyStateMachineTransition.php b/src/Component/src/Metadata/ApplyStateMachineTransition.php index 10488d6c5..9939a4815 100644 --- a/src/Component/src/Metadata/ApplyStateMachineTransition.php +++ b/src/Component/src/Metadata/ApplyStateMachineTransition.php @@ -22,6 +22,7 @@ final class ApplyStateMachineTransition extends HttpOperation implements UpdateO public function __construct( ?array $methods = null, ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?string $template = null, ?string $shortName = null, @@ -44,6 +45,7 @@ public function __construct( parent::__construct( methods: $methods ?? ['PUT', 'PATCH', 'POST'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, template: $template, shortName: $shortName ?? $stateMachineTransition ?? 'apply_state_machine_transition', diff --git a/src/Component/src/Metadata/BulkDelete.php b/src/Component/src/Metadata/BulkDelete.php index dee30e651..111035a1c 100644 --- a/src/Component/src/Metadata/BulkDelete.php +++ b/src/Component/src/Metadata/BulkDelete.php @@ -22,6 +22,7 @@ final class BulkDelete extends HttpOperation implements DeleteOperationInterface public function __construct( ?array $methods = null, ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -45,6 +46,7 @@ public function __construct( parent::__construct( methods: $methods ?? ['DELETE', 'POST'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/BulkUpdate.php b/src/Component/src/Metadata/BulkUpdate.php index fa3d30887..366f3c17d 100644 --- a/src/Component/src/Metadata/BulkUpdate.php +++ b/src/Component/src/Metadata/BulkUpdate.php @@ -22,6 +22,7 @@ final class BulkUpdate extends HttpOperation implements UpdateOperationInterface public function __construct( ?array $methods = null, ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -52,6 +53,7 @@ public function __construct( parent::__construct( methods: $methods ?? ['PUT', 'PATCH'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/Create.php b/src/Component/src/Metadata/Create.php index 9f121ff3b..cda12bd9e 100644 --- a/src/Component/src/Metadata/Create.php +++ b/src/Component/src/Metadata/Create.php @@ -25,6 +25,7 @@ final class Create extends HttpOperation implements CreateOperationInterface, St public function __construct( ?array $methods = null, ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -59,6 +60,7 @@ public function __construct( parent::__construct( methods: $methods ?? ['GET', 'POST'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/Delete.php b/src/Component/src/Metadata/Delete.php index 302ad4f0a..40e742dea 100644 --- a/src/Component/src/Metadata/Delete.php +++ b/src/Component/src/Metadata/Delete.php @@ -22,6 +22,7 @@ final class Delete extends HttpOperation implements DeleteOperationInterface public function __construct( ?array $methods = null, ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -50,6 +51,7 @@ public function __construct( parent::__construct( methods: $methods ?? ['DELETE', 'POST'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/Index.php b/src/Component/src/Metadata/Index.php index 10790bbbf..e2bbe4e88 100644 --- a/src/Component/src/Metadata/Index.php +++ b/src/Component/src/Metadata/Index.php @@ -22,6 +22,7 @@ final class Index extends HttpOperation implements CollectionOperationInterface, public function __construct( ?array $methods = null, ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -50,6 +51,7 @@ public function __construct( parent::__construct( methods: $methods ?? ['GET'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/Show.php b/src/Component/src/Metadata/Show.php index 8a8eeb74f..3e6265888 100644 --- a/src/Component/src/Metadata/Show.php +++ b/src/Component/src/Metadata/Show.php @@ -22,6 +22,7 @@ final class Show extends HttpOperation implements ShowOperationInterface public function __construct( ?array $methods = null, ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -49,6 +50,7 @@ public function __construct( parent::__construct( methods: $methods ?? ['GET'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, diff --git a/src/Component/src/Metadata/Update.php b/src/Component/src/Metadata/Update.php index a62bab59e..0da81b7a2 100644 --- a/src/Component/src/Metadata/Update.php +++ b/src/Component/src/Metadata/Update.php @@ -22,6 +22,7 @@ final class Update extends HttpOperation implements UpdateOperationInterface, St public function __construct( ?array $methods = null, ?string $path = null, + ?string $routeName = null, ?string $routePrefix = null, ?array $routeRequirements = null, ?string $template = null, @@ -53,6 +54,7 @@ public function __construct( parent::__construct( methods: $methods ?? ['GET', 'PUT', 'POST'], path: $path, + routeName: $routeName, routePrefix: $routePrefix, routeRequirements: $routeRequirements, template: $template, From 453067164289221ddbee10e0cd1b1769c4823b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Fri, 28 Feb 2025 09:19:12 +0100 Subject: [PATCH 11/12] Custom notification message --- .../Symfony/Session/Flash/FlashHelperSpec.php | 25 +++++++++++++++++++ src/Component/src/Metadata/Api/Delete.php | 2 ++ src/Component/src/Metadata/Api/Get.php | 2 ++ .../src/Metadata/Api/GetCollection.php | 2 ++ src/Component/src/Metadata/Api/Patch.php | 2 ++ src/Component/src/Metadata/Api/Post.php | 2 ++ src/Component/src/Metadata/Api/Put.php | 2 ++ .../Metadata/ApplyStateMachineTransition.php | 2 ++ src/Component/src/Metadata/BulkDelete.php | 2 ++ src/Component/src/Metadata/BulkUpdate.php | 2 ++ src/Component/src/Metadata/Create.php | 2 ++ src/Component/src/Metadata/Delete.php | 2 ++ src/Component/src/Metadata/HttpOperation.php | 2 ++ src/Component/src/Metadata/Index.php | 2 ++ src/Component/src/Metadata/Operation.php | 14 +++++++++++ src/Component/src/Metadata/Show.php | 2 ++ src/Component/src/Metadata/Update.php | 2 ++ .../src/Symfony/Session/Flash/FlashHelper.php | 2 +- .../Command/DebugResourceCommandTest.php | 1 + 19 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/Component/spec/Symfony/Session/Flash/FlashHelperSpec.php b/src/Component/spec/Symfony/Session/Flash/FlashHelperSpec.php index bd37c37fb..8ef6be2d1 100644 --- a/src/Component/spec/Symfony/Session/Flash/FlashHelperSpec.php +++ b/src/Component/spec/Symfony/Session/Flash/FlashHelperSpec.php @@ -91,6 +91,31 @@ function it_adds_success_flashes_with_fallback_message( $this->addSuccessFlash($operation, $context); } + function it_adds_success_flashes_with_custom_message( + Request $request, + SessionInterface $session, + FlashBagInterface $flashBag, + TranslatorBagInterface $translator, + MessageCatalogueInterface $messageCatalogue, + ): void { + $operation = (new Create(notificationMessage: 'app.dummy.shipped'))->withResource(new ResourceMetadata(alias: 'app.dummy', name: 'dummy', applicationName: 'app')); + $context = new Context(new RequestOption($request->getWrappedObject())); + + $request->getSession()->willReturn($session); + + $session->getBag('flashes')->willReturn($flashBag); + + $translator->getCatalogue()->willReturn($messageCatalogue); + + $messageCatalogue->has('app.dummy.shipped', 'flashes')->willReturn(true)->shouldBeCalled(); + + $translator->trans('app.dummy.shipped', ['%resource%' => 'Dummy'], 'flashes')->willReturn('Dummy was shipped successfully.')->shouldBeCalled(); + + $flashBag->add('success', 'Dummy was shipped successfully.')->shouldBeCalled(); + + $this->addSuccessFlash($operation, $context); + } + function it_adds_success_flashes_with_default_message_when_translator_is_not_a_bag( Request $request, SessionInterface $session, diff --git a/src/Component/src/Metadata/Api/Delete.php b/src/Component/src/Metadata/Api/Delete.php index ca5b21226..63447dcab 100644 --- a/src/Component/src/Metadata/Api/Delete.php +++ b/src/Component/src/Metadata/Api/Delete.php @@ -47,6 +47,7 @@ public function __construct( ?array $denormalizationContext = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, ?string $redirectToRoute = null, ) { parent::__construct( @@ -75,6 +76,7 @@ public function __construct( denormalizationContext: $denormalizationContext, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, redirectToRoute: $redirectToRoute, ); } diff --git a/src/Component/src/Metadata/Api/Get.php b/src/Component/src/Metadata/Api/Get.php index db36d934f..bba4e7196 100644 --- a/src/Component/src/Metadata/Api/Get.php +++ b/src/Component/src/Metadata/Api/Get.php @@ -47,6 +47,7 @@ public function __construct( ?array $denormalizationContext = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, ?string $redirectToRoute = null, ) { parent::__construct( @@ -75,6 +76,7 @@ public function __construct( denormalizationContext: $denormalizationContext, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, redirectToRoute: $redirectToRoute, ); } diff --git a/src/Component/src/Metadata/Api/GetCollection.php b/src/Component/src/Metadata/Api/GetCollection.php index af38a475f..372bf7bce 100644 --- a/src/Component/src/Metadata/Api/GetCollection.php +++ b/src/Component/src/Metadata/Api/GetCollection.php @@ -47,6 +47,7 @@ public function __construct( ?array $denormalizationContext = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, ?string $redirectToRoute = null, ) { parent::__construct( @@ -75,6 +76,7 @@ public function __construct( denormalizationContext: $denormalizationContext, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, redirectToRoute: $redirectToRoute, ); } diff --git a/src/Component/src/Metadata/Api/Patch.php b/src/Component/src/Metadata/Api/Patch.php index eb5349da0..a0e8f3576 100644 --- a/src/Component/src/Metadata/Api/Patch.php +++ b/src/Component/src/Metadata/Api/Patch.php @@ -47,6 +47,7 @@ public function __construct( ?array $denormalizationContext = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, ?string $redirectToRoute = null, ) { parent::__construct( @@ -75,6 +76,7 @@ public function __construct( denormalizationContext: $denormalizationContext, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, redirectToRoute: $redirectToRoute, ); } diff --git a/src/Component/src/Metadata/Api/Post.php b/src/Component/src/Metadata/Api/Post.php index 4aeb18a4c..2a87cbdd6 100644 --- a/src/Component/src/Metadata/Api/Post.php +++ b/src/Component/src/Metadata/Api/Post.php @@ -47,6 +47,7 @@ public function __construct( ?array $denormalizationContext = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, ?string $redirectToRoute = null, ) { parent::__construct( @@ -75,6 +76,7 @@ public function __construct( denormalizationContext: $denormalizationContext, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, redirectToRoute: $redirectToRoute, ); } diff --git a/src/Component/src/Metadata/Api/Put.php b/src/Component/src/Metadata/Api/Put.php index 1a22fc883..b8a9f4ed9 100644 --- a/src/Component/src/Metadata/Api/Put.php +++ b/src/Component/src/Metadata/Api/Put.php @@ -47,6 +47,7 @@ public function __construct( ?array $denormalizationContext = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, ?string $redirectToRoute = null, ) { parent::__construct( @@ -75,6 +76,7 @@ public function __construct( denormalizationContext: $denormalizationContext, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, redirectToRoute: $redirectToRoute, ); } diff --git a/src/Component/src/Metadata/ApplyStateMachineTransition.php b/src/Component/src/Metadata/ApplyStateMachineTransition.php index 9939a4815..8318d4997 100644 --- a/src/Component/src/Metadata/ApplyStateMachineTransition.php +++ b/src/Component/src/Metadata/ApplyStateMachineTransition.php @@ -37,6 +37,7 @@ public function __construct( ?bool $validate = null, ?string $formType = null, ?array $formOptions = null, + ?string $notificationMessage = null, ?string $redirectToRoute = null, private ?string $stateMachineComponent = null, private ?string $stateMachineTransition = null, @@ -60,6 +61,7 @@ public function __construct( validate: $validate ?? false, formType: $formType, formOptions: $formOptions, + notificationMessage: $notificationMessage, redirectToRoute: $redirectToRoute, ); } diff --git a/src/Component/src/Metadata/BulkDelete.php b/src/Component/src/Metadata/BulkDelete.php index 111035a1c..b22724041 100644 --- a/src/Component/src/Metadata/BulkDelete.php +++ b/src/Component/src/Metadata/BulkDelete.php @@ -39,6 +39,7 @@ public function __construct( ?string $formType = null, ?array $formOptions = null, ?string $eventShortName = null, + ?string $notificationMessage = null, ?string $redirectToRoute = null, ?array $redirectArguments = null, ?array $vars = null, @@ -63,6 +64,7 @@ public function __construct( formType: $formType, formOptions: $formOptions, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, redirectToRoute: $redirectToRoute, redirectArguments: $redirectArguments, vars: $vars, diff --git a/src/Component/src/Metadata/BulkUpdate.php b/src/Component/src/Metadata/BulkUpdate.php index 366f3c17d..3ddccfb70 100644 --- a/src/Component/src/Metadata/BulkUpdate.php +++ b/src/Component/src/Metadata/BulkUpdate.php @@ -43,6 +43,7 @@ public function __construct( ?array $formOptions = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, ?string $redirectToRoute = null, ?array $redirectArguments = null, ?array $vars = null, @@ -74,6 +75,7 @@ public function __construct( formOptions: $formOptions, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, redirectToRoute: $redirectToRoute, redirectArguments: $redirectArguments, vars: $vars, diff --git a/src/Component/src/Metadata/Create.php b/src/Component/src/Metadata/Create.php index cda12bd9e..90d2ce372 100644 --- a/src/Component/src/Metadata/Create.php +++ b/src/Component/src/Metadata/Create.php @@ -49,6 +49,7 @@ public function __construct( ?array $formOptions = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, string|callable|null $twigContextFactory = null, ?string $redirectToRoute = null, ?array $redirectArguments = null, @@ -81,6 +82,7 @@ public function __construct( formOptions: $formOptions, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, twigContextFactory: $twigContextFactory, redirectToRoute: $redirectToRoute, redirectArguments: $redirectArguments, diff --git a/src/Component/src/Metadata/Delete.php b/src/Component/src/Metadata/Delete.php index 40e742dea..91929c5d3 100644 --- a/src/Component/src/Metadata/Delete.php +++ b/src/Component/src/Metadata/Delete.php @@ -43,6 +43,7 @@ public function __construct( ?array $formOptions = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, string|callable|null $twigContextFactory = null, ?string $redirectToRoute = null, ?array $redirectArguments = null, @@ -72,6 +73,7 @@ public function __construct( formOptions: $formOptions, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, twigContextFactory: $twigContextFactory, redirectToRoute: $redirectToRoute, redirectArguments: $redirectArguments, diff --git a/src/Component/src/Metadata/HttpOperation.php b/src/Component/src/Metadata/HttpOperation.php index a2dab3c52..cb16b5be9 100644 --- a/src/Component/src/Metadata/HttpOperation.php +++ b/src/Component/src/Metadata/HttpOperation.php @@ -47,6 +47,7 @@ public function __construct( ?array $denormalizationContext = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, string|callable|null $twigContextFactory = null, protected ?string $redirectToRoute = null, protected ?array $redirectArguments = null, @@ -73,6 +74,7 @@ public function __construct( denormalizationContext: $denormalizationContext, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, ); $this->twigContextFactory = $twigContextFactory; diff --git a/src/Component/src/Metadata/Index.php b/src/Component/src/Metadata/Index.php index e2bbe4e88..7e1cadbd3 100644 --- a/src/Component/src/Metadata/Index.php +++ b/src/Component/src/Metadata/Index.php @@ -42,6 +42,7 @@ public function __construct( ?string $formType = null, ?array $formOptions = null, ?string $eventShortName = null, + ?string $notificationMessage = null, ?array $validationContext = null, string|callable|null $twigContextFactory = null, ?string $redirectToRoute = null, @@ -72,6 +73,7 @@ public function __construct( formOptions: $formOptions, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, twigContextFactory: $twigContextFactory, redirectToRoute: $redirectToRoute, vars: $vars, diff --git a/src/Component/src/Metadata/Operation.php b/src/Component/src/Metadata/Operation.php index e47e89fc2..d1b70282a 100644 --- a/src/Component/src/Metadata/Operation.php +++ b/src/Component/src/Metadata/Operation.php @@ -53,6 +53,7 @@ public function __construct( protected ?array $denormalizationContext = null, protected ?array $validationContext = null, protected ?string $eventShortName = null, + protected ?string $notificationMessage = null, ) { $this->provider = $provider; $this->processor = $processor; @@ -332,4 +333,17 @@ public function withEventShortName(string $eventShortName): self return $self; } + + public function getNotificationMessage(): ?string + { + return $this->notificationMessage; + } + + public function withNotificationMessage(string $notificationMessage): self + { + $self = clone $this; + $self->notificationMessage = $notificationMessage; + + return $self; + } } diff --git a/src/Component/src/Metadata/Show.php b/src/Component/src/Metadata/Show.php index 3e6265888..79351610c 100644 --- a/src/Component/src/Metadata/Show.php +++ b/src/Component/src/Metadata/Show.php @@ -43,6 +43,7 @@ public function __construct( ?array $formOptions = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, string|callable|null $twigContextFactory = null, ?string $redirectToRoute = null, ?array $vars = null, @@ -71,6 +72,7 @@ public function __construct( formOptions: $formOptions, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, twigContextFactory: $twigContextFactory, redirectToRoute: $redirectToRoute, vars: $vars, diff --git a/src/Component/src/Metadata/Update.php b/src/Component/src/Metadata/Update.php index 0da81b7a2..28a09f04f 100644 --- a/src/Component/src/Metadata/Update.php +++ b/src/Component/src/Metadata/Update.php @@ -43,6 +43,7 @@ public function __construct( ?array $formOptions = null, ?array $validationContext = null, ?string $eventShortName = null, + ?string $notificationMessage = null, string|callable|null $twigContextFactory = null, ?string $redirectToRoute = null, ?array $redirectArguments = null, @@ -75,6 +76,7 @@ public function __construct( formOptions: $formOptions, validationContext: $validationContext, eventShortName: $eventShortName, + notificationMessage: $notificationMessage, twigContextFactory: $twigContextFactory, redirectToRoute: $redirectToRoute, redirectArguments: $redirectArguments, diff --git a/src/Component/src/Symfony/Session/Flash/FlashHelper.php b/src/Component/src/Symfony/Session/Flash/FlashHelper.php index ca3698b67..770608fb9 100644 --- a/src/Component/src/Symfony/Session/Flash/FlashHelper.php +++ b/src/Component/src/Symfony/Session/Flash/FlashHelper.php @@ -74,7 +74,7 @@ private function buildOperationMessage(Operation $operation, string $type): stri $resource = $operation->getResource(); Assert::notNull($resource); - $key = sprintf('%s.%s.%s', $resource->getApplicationName() ?? '', $resource->getName() ?? '', $operation->getShortName() ?? ''); + $key = $operation->getNotificationMessage() ?? sprintf('%s.%s.%s', $resource->getApplicationName() ?? '', $resource->getName() ?? '', $operation->getShortName() ?? ''); $fallbackKey = sprintf('sylius.resource.%s', $operation->getShortName() ?? ''); $parameters = $this->getTranslationParameters($operation); diff --git a/tests/Bundle/Command/DebugResourceCommandTest.php b/tests/Bundle/Command/DebugResourceCommandTest.php index 1d46f2b4e..aaeb12b8d 100644 --- a/tests/Bundle/Command/DebugResourceCommandTest.php +++ b/tests/Bundle/Command/DebugResourceCommandTest.php @@ -309,6 +309,7 @@ public function it_displays_the_metadata_for_given_resource_operation(): void denormalizationContext null validationContext null eventShortName "register" + notificationMessage null ------------------------ -------------------------- From eb0dab26441e481094d95d58eeb1b421389f6747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Wed, 5 Mar 2025 08:26:30 +0100 Subject: [PATCH 12/12] PoC Console operations --- .../Resources/config/services/console.xml | 15 ++ src/Bundle/SyliusResourceBundle.php | 2 + src/Component/src/Metadata/Metadata.php | 2 +- ...butesResourceMetadataCollectionFactory.php | 28 +++ ...viderResourceMetadataCollectionFactory.php | 17 +- .../src/State/Processor/RespondProcessor.php | 6 +- .../Symfony/Console/Context/ConsoleOption.php | 43 +++++ .../src/Symfony/Console/Operation/Browse.php | 74 ++++++++ .../Console/Operation/ConsoleOperation.php | 71 ++++++++ .../src/Symfony/Console/Operation/Read.php | 59 ++++++ .../src/Symfony/Console/ResourceCommand.php | 87 +++++++++ .../Console/State/ConsoleGridProvider.php | 97 ++++++++++ .../Compiler/RegisterResourceCommandsPass.php | 168 ++++++++++++++++++ .../src/Subscription/Entity/Subscription.php | 18 ++ .../State/BrowseSubscriptionsResponder.php | 54 ++++++ .../State/ShowSubscriptionResponder.php | 47 +++++ .../State/SubscriptionItemProvider.php | 38 ++++ 17 files changed, 817 insertions(+), 9 deletions(-) create mode 100644 src/Component/src/Symfony/Console/Context/ConsoleOption.php create mode 100644 src/Component/src/Symfony/Console/Operation/Browse.php create mode 100644 src/Component/src/Symfony/Console/Operation/ConsoleOperation.php create mode 100644 src/Component/src/Symfony/Console/Operation/Read.php create mode 100644 src/Component/src/Symfony/Console/ResourceCommand.php create mode 100644 src/Component/src/Symfony/Console/State/ConsoleGridProvider.php create mode 100644 src/Component/src/Symfony/DependencyInjection/Compiler/RegisterResourceCommandsPass.php create mode 100644 tests/Application/src/Subscription/State/BrowseSubscriptionsResponder.php create mode 100644 tests/Application/src/Subscription/State/ShowSubscriptionResponder.php create mode 100644 tests/Application/src/Subscription/State/SubscriptionItemProvider.php diff --git a/src/Bundle/Resources/config/services/console.xml b/src/Bundle/Resources/config/services/console.xml index 6e9223cd0..0c1e4c6ec 100644 --- a/src/Bundle/Resources/config/services/console.xml +++ b/src/Bundle/Resources/config/services/console.xml @@ -21,5 +21,20 @@ + + + + + + + + + + + + + + + diff --git a/src/Bundle/SyliusResourceBundle.php b/src/Bundle/SyliusResourceBundle.php index 257530f2a..c3d0a017f 100644 --- a/src/Bundle/SyliusResourceBundle.php +++ b/src/Bundle/SyliusResourceBundle.php @@ -30,6 +30,7 @@ use Sylius\Bundle\ResourceBundle\DependencyInjection\Compiler\WinzouStateMachinePass; use Sylius\Bundle\ResourceBundle\DependencyInjection\PagerfantaExtension; use Sylius\Resource\Symfony\DependencyInjection\Compiler\DisableMetadataCachePass; +use Sylius\Resource\Symfony\DependencyInjection\Compiler\RegisterResourceCommandsPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -50,6 +51,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new CsrfTokenManagerPass()); $container->addCompilerPass(new DisableMetadataCachePass()); + $container->addCompilerPass(new RegisterResourceCommandsPass()); $container->addCompilerPass(new DoctrineContainerRepositoryFactoryPass()); $container->addCompilerPass(new DoctrineTargetEntitiesResolverPass(new TargetEntitiesResolver()), PassConfig::TYPE_BEFORE_OPTIMIZATION, 1); $container->addCompilerPass(new RegisterFormBuilderPass()); diff --git a/src/Component/src/Metadata/Metadata.php b/src/Component/src/Metadata/Metadata.php index 9bedda3d8..b12e49da7 100644 --- a/src/Component/src/Metadata/Metadata.php +++ b/src/Component/src/Metadata/Metadata.php @@ -40,7 +40,7 @@ private function __construct(string $name, string $applicationName, array $param $this->name = $name; $this->applicationName = $applicationName; - $this->driver = $parameters['driver']; + $this->driver = $parameters['driver'] ?? false; $this->templatesNamespace = array_key_exists('templates', $parameters) ? $parameters['templates'] : null; $this->stateMachineComponent = $parameters['state_machine_component'] ?? null; diff --git a/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index e6b8b4df0..3ea2ccab8 100644 --- a/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Component/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -15,6 +15,7 @@ use Sylius\Resource\Metadata\AsResource; use Sylius\Resource\Metadata\HttpOperation; +use Sylius\Resource\Metadata\Metadata; use Sylius\Resource\Metadata\MetadataInterface; use Sylius\Resource\Metadata\Operation; use Sylius\Resource\Metadata\Operations; @@ -22,6 +23,7 @@ use Sylius\Resource\Metadata\Resource\ResourceMetadataCollection; use Sylius\Resource\Metadata\ResourceMetadata; use Sylius\Resource\Reflection\ClassReflection; +use Sylius\Resource\Symfony\Console\Operation\ConsoleOperation; use Sylius\Resource\Symfony\Request\State\Responder; use Sylius\Resource\Symfony\Routing\Factory\RouteName\OperationRouteNameFactory; @@ -208,11 +210,37 @@ private function getOperationWithDefaults(ResourceMetadata $resource, Operation $operation = $operation->withName($routeName); } + if ($operation instanceof ConsoleOperation) { + if (null === $commandName = $operation->getCommandName()) { + $commandName = $this->createCommandName($resource, $operation); + $operation = $operation->withCommandName($commandName); + } + + if (null === $operation->getResponder()) { + $operation = $operation->withResponder(Responder::class); + } + + $operation = $operation->withName($commandName); + } + $operationName = $operation->getName(); return [$operationName, $operation]; } + private function createCommandName(ResourceMetadata $resourceMetadata, ConsoleOperation $operation): string + { + $metadata = Metadata::fromAliasAndConfiguration($resourceMetadata->getAlias() ?? '', []); + $section = $resourceMetadata->getSection(); + + return sprintf( + '%s:%s-%s', + (null !== $section ? $section . ':' : '') . $metadata->getApplicationName(), + $operation->getShortName() ?? '', + $metadata->getName(), + ); + } + private function buildFormOptions(Operation $operation, MetadataInterface $resourceConfiguration): array { $formOptions = array_merge( diff --git a/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php index 13045c05f..ed0ca65ff 100644 --- a/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php +++ b/src/Component/src/Metadata/Resource/Factory/ProviderResourceMetadataCollectionFactory.php @@ -15,10 +15,13 @@ use Sylius\Resource\Grid\State\RequestGridProvider; use Sylius\Resource\Metadata\GridAwareOperationInterface; +use Sylius\Resource\Metadata\HttpOperation; use Sylius\Resource\Metadata\Operation; use Sylius\Resource\Metadata\Operations; use Sylius\Resource\Metadata\Resource\ResourceMetadataCollection; use Sylius\Resource\Metadata\ResourceMetadata; +use Sylius\Resource\Symfony\Console\Operation\ConsoleOperation; +use Sylius\Resource\Symfony\Console\State\ConsoleGridProvider; final class ProviderResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { @@ -54,13 +57,21 @@ public function create(string $resourceClass): ResourceMetadataCollection private function addDefaults(Operation $operation): Operation { if ( - null === $operation->getProvider() && - $operation instanceof GridAwareOperationInterface && - null !== $operation->getGrid() + null !== $operation->getProvider() || + !$operation instanceof GridAwareOperationInterface || + null === $operation->getGrid() ) { + return $operation; + } + + if ($operation instanceof HttpOperation) { $operation = $operation->withProvider(RequestGridProvider::class); } + if ($operation instanceof ConsoleOperation) { + $operation = $operation->withProvider(ConsoleGridProvider::class); + } + return $operation; } } diff --git a/src/Component/src/State/Processor/RespondProcessor.php b/src/Component/src/State/Processor/RespondProcessor.php index 162baac15..9f7320165 100644 --- a/src/Component/src/State/Processor/RespondProcessor.php +++ b/src/Component/src/State/Processor/RespondProcessor.php @@ -18,7 +18,6 @@ use Sylius\Resource\State\ProcessorInterface; use Sylius\Resource\State\ResponderInterface; use Symfony\Component\HttpFoundation\Response; -use Webmozart\Assert\Assert; /** * @experimental @@ -36,9 +35,6 @@ public function process(mixed $data, Operation $operation, Context $context): mi return $data; } - $response = $this->responder->respond($data, $operation, $context); - Assert::isInstanceOf($response, Response::class); - - return $response; + return $this->responder->respond($data, $operation, $context); } } diff --git a/src/Component/src/Symfony/Console/Context/ConsoleOption.php b/src/Component/src/Symfony/Console/Context/ConsoleOption.php new file mode 100644 index 000000000..b481ce10b --- /dev/null +++ b/src/Component/src/Symfony/Console/Context/ConsoleOption.php @@ -0,0 +1,43 @@ +command; + } + + public function input(): InputInterface + { + return $this->input; + } + + public function output(): OutputInterface + { + return $this->output; + } +} diff --git a/src/Component/src/Symfony/Console/Operation/Browse.php b/src/Component/src/Symfony/Console/Operation/Browse.php new file mode 100644 index 000000000..b21f1da80 --- /dev/null +++ b/src/Component/src/Symfony/Console/Operation/Browse.php @@ -0,0 +1,74 @@ +grid; + } + + public function withGrid(string $grid): self + { + $self = clone $this; + $self->grid = $grid; + + return $self; + } +} diff --git a/src/Component/src/Symfony/Console/Operation/ConsoleOperation.php b/src/Component/src/Symfony/Console/Operation/ConsoleOperation.php new file mode 100644 index 000000000..8d8b9aa3b --- /dev/null +++ b/src/Component/src/Symfony/Console/Operation/ConsoleOperation.php @@ -0,0 +1,71 @@ +commandName; + } + + public function withCommandName(string $commandName): self + { + $self = clone $this; + $self->commandName = $commandName; + + return $self; + } +} diff --git a/src/Component/src/Symfony/Console/Operation/Read.php b/src/Component/src/Symfony/Console/Operation/Read.php new file mode 100644 index 000000000..16ecd56c1 --- /dev/null +++ b/src/Component/src/Symfony/Console/Operation/Read.php @@ -0,0 +1,59 @@ +addOption('id', null, InputOption::VALUE_REQUIRED, 'The resource id.') + ->addOption('resource', null, InputOption::VALUE_REQUIRED, 'Resource alias or Resource FQCN.') + ->addOption('operation', null, InputOption::VALUE_REQUIRED, 'Operation name.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $resource = $this->resource ?? $input->getOption('resource'); + + if (str_contains($resource, '.')) { + $metadata = $this->registry->get($resource); + } else { + $metadata = $this->registry->getByClass($resource); + } + + $resourceMetadataCollection = $this->getResourceMetadataCollection($metadata); + + $operation = $resourceMetadataCollection->getOperation($metadata->getAlias(), $this->createOperationName($input)); + + $context = new Context(new ConsoleOption($this, $input, $output)); + + $data = $this->provider->provide($operation, $context); + + return $this->processor->process($data, $operation, $context); + } + + private function getResourceMetadataCollection(MetadataInterface $resourceConfiguration): ResourceMetadataCollection + { + return $this->resourceMetadataCollectionFactory->create($resourceConfiguration->getClass('model')); + } + + private function createOperationName(InputInterface $input): string + { + $operationName = $this->getName() ?? $input->getOption('operation'); + + if (null !== $operationName) { + return $operationName; + } + + return ''; + } +} diff --git a/src/Component/src/Symfony/Console/State/ConsoleGridProvider.php b/src/Component/src/Symfony/Console/State/ConsoleGridProvider.php new file mode 100644 index 000000000..c437f57a5 --- /dev/null +++ b/src/Component/src/Symfony/Console/State/ConsoleGridProvider.php @@ -0,0 +1,97 @@ +gridViewFactory || null === $this->gridProvider) { + throw new \LogicException('You can not use a grid if Sylius Grid Bundle is not available. Try running "composer require sylius/grid-bundle".'); + } + + if (!$operation instanceof GridAwareOperationInterface) { + throw new \LogicException(sprintf('You can not use a grid if your operation does not implement "%s".', GridAwareOperationInterface::class)); + } + + $grid = $operation->getGrid(); + + if (null === $grid) { + throw new \RuntimeException(sprintf('Operation has no grid, so you cannot use this provider for operation "%s"', $operation->getName() ?? '')); + } + + $input = $context->get(ConsoleOption::class)?->input(); + + if (null === $input) { + return null; + } + + $gridDefinition = $this->gridProvider->get($grid); + $gridConfiguration = $gridDefinition->getDriverConfiguration(); + + $parameters = []; + $gridView = $this->gridViewFactory->create($gridDefinition, $context, new Parameters($parameters), $gridConfiguration); + + $data = $gridView->getData(); + + if ($data instanceof PagerfantaInterface) { + $currentPage = $input->hasOption('page') ? $input->getOption('page') : 1; + $data->setCurrentPage($currentPage); + + $maxPerPage = $this->resolveMaxPerPage( + $input->hasOption('limit') ? $input->getOption('limit') : null, + $gridDefinition->getLimits(), + ); + $data->setMaxPerPage($maxPerPage); + } + + return $gridView; + } + + private function resolveMaxPerPage(?int $requestLimit, array $gridLimits = []): int + { + if (null === $requestLimit) { + $firstGridLimit = reset($gridLimits); + + return false === $firstGridLimit ? self::DEFAULT_MAX_PER_PAGE : $firstGridLimit; + } + + if (!empty($gridLimits)) { + $maxGridLimit = max($gridLimits); + + // Cannot retrieve more items than configured in the grid + return min($requestLimit, $maxGridLimit); + } + + return $requestLimit; + } +} diff --git a/src/Component/src/Symfony/DependencyInjection/Compiler/RegisterResourceCommandsPass.php b/src/Component/src/Symfony/DependencyInjection/Compiler/RegisterResourceCommandsPass.php new file mode 100644 index 000000000..1af818369 --- /dev/null +++ b/src/Component/src/Symfony/DependencyInjection/Compiler/RegisterResourceCommandsPass.php @@ -0,0 +1,168 @@ +getDefinition('sylius.console.command.resource'); + + $mapping = $container->getParameter('sylius.resource.mapping'); + + /** @var string[] $paths */ + $paths = $mapping['paths'] ?? []; + + $resourcesConfig = $container->hasParameter('sylius.resources') ? $container->getParameter('sylius.resources') : []; + + foreach (ClassReflection::getResourcesByPaths($paths) as $className) { + $alias = $this->createResourceAlias($className, $resourcesConfig); + $this->registerCommandsForClassName($container, $definition, $className, $alias); + } + } + + /** + * @param class-string $className + */ + private function createResourceAlias(string $className, array $resourcesConfig): string + { + foreach ($resourcesConfig as $alias => $resourceConfig) { + if ($className === ($resourceConfig['classes']['model'] ?? null)) { + return $alias; + } + } + + return $this->getDefaultAlias($className); + } + + /** + * @param class-string $className + */ + private function registerCommandsForClassName( + ContainerBuilder $container, + Definition $definition, + string $className, + ?string $alias, + ): void { + $resourceMetadataCollection = $this->buildResourceMetadataCollection($className, $alias); + + /** @var ResourceMetadata $resourceMetadata */ + foreach ($resourceMetadataCollection as $resourceMetadata) { + $this->registerCommandsForClassNameAndOperations($container, $definition, $className, $resourceMetadata->getOperations() ?? new Operations()); + } + } + + private function registerCommandsForClassNameAndOperations( + ContainerBuilder $container, + Definition $definition, + string $className, + Operations $operations, + ): void { + foreach ($operations as $operation) { + if (!$operation instanceof ConsoleOperation) { + continue; + } + + $clonedDefinition = clone $definition; + + $clonedDefinition->addArgument($className); + $clonedDefinition->addArgument($operation->getName()); + + $container->setDefinition(sprintf('_resource.%s.%s', $className, uniqid()), $clonedDefinition); + } + } + + /** + * @param class-string $className + */ + private function buildResourceMetadataCollection(string $className, ?string $alias): ResourceMetadataCollection + { + $resourceMetadataCollection = new ResourceMetadataCollection(); + $operations = new Operations(); + + foreach (ClassReflection::getClassAttributes($className, AsResource::class) as $resourceAttribute) { + /** @var AsResource $resource */ + $resource = $resourceAttribute->newInstance(); + $resourceMetadata = $resource->toMetadata(); + + /** @var Operation $operation */ + foreach ($resourceMetadata->getOperations() ?? new Operations() as $operation) { + if (!$operation instanceof ConsoleOperation) { + continue; + } + + if (null === $resourceMetadata->getAlias()) { + $resourceMetadata = $resourceMetadata->withAlias($alias); + } + + $operation = $operation->withResource($resourceMetadata); + + $commandName = $this->createCommandName($resourceMetadata, $operation); + $operation = $operation->withName($commandName); + + $operations->add($commandName, $operation); + } + + $resourceMetadataCollection[] = $resourceMetadata->withOperations($operations); + } + + return $resourceMetadataCollection; + } + + private function createCommandName(ResourceMetadata $resourceMetadata, ConsoleOperation $operation): string + { + $operationName = $operation->getName(); + + if (null !== $operationName) { + return $operationName; + } + + $metadata = Metadata::fromAliasAndConfiguration($resourceMetadata->getAlias() ?? '', []); + $section = $resourceMetadata->getSection(); + + return sprintf( + '%s:%s-%s', + (null !== $section ? $section . ':' : '') . $metadata->getApplicationName(), + $operation->getShortName() ?? '', + $metadata->getName(), + ); + } + + /** @param class-string $className */ + private function getDefaultAlias(string $className): string + { + $reflectionClass = new \ReflectionClass($className); + + $shortName = $reflectionClass->getShortName(); + $suffix = 'Resource'; + if (str_ends_with($shortName, $suffix)) { + $shortName = substr($shortName, 0, strlen($shortName) - strlen($suffix)); + } + + return 'app.' . u($shortName)->snake()->toString(); + } +} diff --git a/tests/Application/src/Subscription/Entity/Subscription.php b/tests/Application/src/Subscription/Entity/Subscription.php index e721ece8e..29e8de8c2 100644 --- a/tests/Application/src/Subscription/Entity/Subscription.php +++ b/tests/Application/src/Subscription/Entity/Subscription.php @@ -16,6 +16,9 @@ use App\Subscription\Factory\SubscriptionFactory; use App\Subscription\Form\Type\SubscriptionType; use App\Subscription\Repository\SubscriptionRepository; +use App\Subscription\State\BrowseSubscriptionsResponder; +use App\Subscription\State\ShowSubscriptionResponder; +use App\Subscription\State\SubscriptionItemProvider; use App\Subscription\Twig\Context\Factory\ShowSubscriptionContextFactory; use Doctrine\ORM\Mapping as ORM; use Sylius\Resource\Metadata\Api; @@ -29,6 +32,8 @@ use Sylius\Resource\Metadata\Show; use Sylius\Resource\Metadata\Update; use Sylius\Resource\Model\ResourceInterface; +use Sylius\Resource\Symfony\Console\Operation\Browse; +use Sylius\Resource\Symfony\Console\Operation\Read; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; @@ -56,6 +61,19 @@ twigContextFactory: ShowSubscriptionContextFactory::class, )] +#[AsResource( + operations: [ + new Read( + provider: SubscriptionItemProvider::class, + responder: ShowSubscriptionResponder::class, + ), + new Browse( + grid: 'app_subscription', + responder: BrowseSubscriptionsResponder::class, + ), + ], +)] + #[AsResource( alias: 'app.subscription', section: 'ajax', diff --git a/tests/Application/src/Subscription/State/BrowseSubscriptionsResponder.php b/tests/Application/src/Subscription/State/BrowseSubscriptionsResponder.php new file mode 100644 index 000000000..7d8e06dfe --- /dev/null +++ b/tests/Application/src/Subscription/State/BrowseSubscriptionsResponder.php @@ -0,0 +1,54 @@ +get(ConsoleOption::class); + Assert::notNull($consoleOption); + + $ui = new SymfonyStyle($consoleOption->input(), $consoleOption->output()); + + Assert::isInstanceOf($data, GridView::class); + Assert::isInstanceOf($data->getData(), PagerfantaInterface::class); + Assert::allIsInstanceOf($data->getData(), Subscription::class); + + /** @var Subscription $subscription */ + foreach ($data->getData() as $subscription) { + $ui->section('Id'); + $ui->writeln((string) $subscription->getId()); + + $ui->section('State'); + $ui->writeln($subscription->getState()); + } + + return Command::SUCCESS; + } +} diff --git a/tests/Application/src/Subscription/State/ShowSubscriptionResponder.php b/tests/Application/src/Subscription/State/ShowSubscriptionResponder.php new file mode 100644 index 000000000..56de513c9 --- /dev/null +++ b/tests/Application/src/Subscription/State/ShowSubscriptionResponder.php @@ -0,0 +1,47 @@ +get(ConsoleOption::class); + Assert::notNull($consoleOption); + + $ui = new SymfonyStyle($consoleOption->input(), $consoleOption->output()); + + Assert::isInstanceOf($data, Subscription::class); + + $ui->section('Id'); + $ui->writeln((string) $data->getId()); + + $ui->section('State'); + $ui->writeln($data->getState()); + + return Command::SUCCESS; + } +} diff --git a/tests/Application/src/Subscription/State/SubscriptionItemProvider.php b/tests/Application/src/Subscription/State/SubscriptionItemProvider.php new file mode 100644 index 000000000..4d4c60575 --- /dev/null +++ b/tests/Application/src/Subscription/State/SubscriptionItemProvider.php @@ -0,0 +1,38 @@ +get(ConsoleOption::class)?->input()->getOption('id'); + Assert::notNull($id); + + return $this->entityManager->getRepository(Subscription::class)->find($id); + } +}