diff --git a/docs/show_resource.md b/docs/show_resource.md index 6033e4294..49f21b181 100644 --- a/docs/show_resource.md +++ b/docs/show_resource.md @@ -23,7 +23,7 @@ app_book_show:
PHP ```php -# src/Entity/Book +// src/Entity/Book use Sylius\Component\Resource\Annotation\SyliusRoute; diff --git a/docs/update_resource.md b/docs/update_resource.md index f5edea794..a0f70a0d3 100644 --- a/docs/update_resource.md +++ b/docs/update_resource.md @@ -2,6 +2,8 @@ To display an edit form of a particular resource, change it or update it via API, you should use the **updateAction** action of your **app.controller.book** service. +
Yaml + ```yaml # config/routes.yaml @@ -11,6 +13,26 @@ app_book_update: defaults: _controller: app.controller.book::updateAction ``` + +
+ +
PHP + +```php +// src/Entity/Book + +use Sylius\Component\Resource\Annotation\SyliusRoute; + +#[SyliusRoute( + name: 'app_book_update', + path: '/books/{id}/edit', + methods: ['GET', 'PUT'], + controller: 'app.controller.book::updateAction', +)] +``` + +
+ Done! Now when you go to ``/books/5/edit``, ResourceController will use the repository (``app.repository.book``) to find the book with id == **5**. If found it will create the ``app_book`` form, and set the existing book as data. @@ -31,6 +53,8 @@ When validation fails, it will simply render the form again, but with error mess Just like for other actions, you can customize the template. +
Yaml + ```yaml # config/routes.yaml @@ -43,10 +67,32 @@ app_book_update: template: Admin/Book/update.html.twig ``` +
+ +
PHP + +```php +// src/Entity/Book + +use Sylius\Component\Resource\Annotation\SyliusRoute; + +#[SyliusRoute( + name: 'app_book_update', + path: '/books/{id}/edit', + methods: ['GET', 'PUT'], + controller: 'app.controller.book::updateAction', + template: 'Admin/book/update.html.twig', +)] +``` + +
+ ## Using Custom Form Same way like for **createAction** you can override the default form. +
Yaml + ```yaml # config/routes.yaml @@ -58,12 +104,36 @@ app_book_update: _sylius: form: App\Form\BookType ``` + +
+ +
PHP + +```php +// src/Entity/Book + +use App\Form\BookType; +use Sylius\Component\Resource\Annotation\SyliusRoute; + +#[SyliusRoute( + name: 'app_book_update', + path: '/books/{id}/edit', + methods: ['GET', 'PUT'], + controller: 'app.controller.book::updateAction', + form: BookType::class, +)] +``` + +
+ ## Passing Custom Options to Form Same way like for **createAction** you can pass options to the form. Below you can see how to specify custom options, in this case, ``validation_groups``, but you can pass any option accepted by the form. +
Yaml + ```yaml # config/routes.yaml @@ -79,10 +149,36 @@ app_book_update: validation_groups: [sylius, my_custom_group] ``` +
+ +
PHP + +```php +// src/Entity/Book + +use App\Form\BookType; +use Sylius\Component\Resource\Annotation\SyliusRoute; + +#[SyliusRoute( + name: 'app_book_update', + path: '/books/{id}/edit', + methods: ['GET', 'PUT'], + controller: 'app.controller.book::updateAction', + form: [ + 'type' => BookType::class, + 'validation_groups' => ['sylius', 'my_custom_group'], + ], +)] +``` + +
+ ## Overriding the Criteria By default, the **updateAction** will look for the resource by id. You can easily change that criteria. +
Yaml + ```yaml # config/routes.yaml @@ -95,10 +191,35 @@ app_book_update: criteria: { title: $title } ``` +
+ +
PHP + +```php +// src/Entity/Book + +use App\Form\BookType; +use Sylius\Component\Resource\Annotation\SyliusRoute; + +#[SyliusRoute( + name: 'app_book_update', + path: '/books/{id}/edit', + methods: ['GET', 'PUT'], + controller: 'app.controller.book::updateAction', + criteria: [ + 'title' => '$title', + ], +)] +``` + +
+ ## Custom Redirect After Success By default the controller will try to get the id of resource and redirect to the "show" route. To change that, use the following configuration. +
Yaml + ```yaml # config/routes.yaml @@ -110,8 +231,32 @@ app_book_update: _sylius: redirect: app_book_index ``` + +
+ +
PHP + +```php +// src/Entity/Book + +use App\Form\BookType; +use Sylius\Component\Resource\Annotation\SyliusRoute; + +#[SyliusRoute( + name: 'app_book_update', + path: '/books/{id}/edit', + methods: ['GET', 'PUT'], + controller: 'app.controller.book::updateAction', + redirect: 'app_book_index', +)] +``` + +
+ You can also perform more complex redirects, with parameters. For example: +
Yaml + ```yaml # config/routes.yaml @@ -126,12 +271,38 @@ app_book_update: parameters: { id: $genreId } ``` +
+ +
PHP + +```php +// src/Entity/Book + +use App\Form\BookType; +use Sylius\Component\Resource\Annotation\SyliusRoute; + +#[SyliusRoute( + name: 'app_book_update', + path: '/genre/{genreId}/books/{id}/edit', + methods: ['GET', 'PUT'], + controller: 'app.controller.book::updateAction', + redirect: [ + 'route' => 'app_genre_show', + 'parameters' => ['id' => '$genreId'], + ], +)] +``` + +
+ ## Custom Event Name By default, there are two events dispatched during resource update, one before setting new data, the other after successful update. The pattern is always the same - ``{applicationName}.{resourceName}.pre/post_update``. However, you can customize the last part of the event, to provide your own action name. +
Yaml + ```yaml # config/routes.yaml @@ -143,6 +314,28 @@ app_book_customer_update: _sylius: event: customer_update ``` + +
+ +
PHP + +```php +// src/Entity/Book + +use App\Form\BookType; +use Sylius\Component\Resource\Annotation\SyliusRoute; + +#[SyliusRoute( + name: 'app_book_customer_update', + path: '/customer/book-update/{id}', + methods: ['GET', 'PUT'], + controller: 'app.controller.book::updateAction', + event: 'customer_update', +)] +``` + +
+ This way, you can listen to ``app.book.pre_customer_update`` and ``app.book.post_customer_update`` events. It's especially useful, when you use ``ResourceController:updateAction`` in more than one route. @@ -152,6 +345,8 @@ This way, you can listen to ``app.book.pre_customer_update`` and ``app.book.post Depending on your app approach it can be useful to return a changed object or only the ``204 HTTP Code``, which indicates that everything worked smoothly. Sylius, by default is returning the ``204 HTTP Code``, which indicates an empty response. If you would like to receive a whole object as a response you should set a ``return_content`` option to true. +
Yaml + ```yaml # config/routes.yaml @@ -165,6 +360,28 @@ app_book_update: return_content: true ``` +
+ +
PHP + +```php +// src/Entity/Book + +use App\Form\BookType; +use Sylius\Component\Resource\Annotation\SyliusRoute; + +#[SyliusRoute( + name: 'app_book_update', + path: '/books/{title}/edit', + methods: ['GET', 'PUT'], + controller: 'app.controller.book::updateAction', + criteria: ['title' => '$title'], + returnContent: true, +)] +``` + +
+ ### **Warning** The ``return_content`` flag is available for the ``applyStateMachineTransitionAction`` method as well. But these are the only ones which can be configured this way. It is worth noticing, that the ``applyStateMachineTransitionAction`` returns a default ``200 HTTP Code`` response with a fully serialized object. diff --git a/src/Bundle/Routing/RouteAttributesFactory.php b/src/Bundle/Routing/RouteAttributesFactory.php index cf37b8b39..ceaf0dd42 100644 --- a/src/Bundle/Routing/RouteAttributesFactory.php +++ b/src/Bundle/Routing/RouteAttributesFactory.php @@ -60,6 +60,38 @@ public function createRouteForClass(RouteCollection $routeCollection, string $cl $syliusOptions['form'] = $arguments['form']; } + if (isset($arguments['section'])) { + $syliusOptions['section'] = $arguments['section']; + } + + if (isset($arguments['permission'])) { + $syliusOptions['permission'] = $arguments['permission']; + } + + if (isset($arguments['grid'])) { + $syliusOptions['grid'] = $arguments['grid']; + } + + if (isset($arguments['csrfProtection'])) { + $syliusOptions['csrf_protection'] = $arguments['csrfProtection']; + } + + if (isset($arguments['redirect'])) { + $syliusOptions['redirect'] = $arguments['redirect']; + } + + if (isset($arguments['stateMachine'])) { + $syliusOptions['state_machine'] = $arguments['stateMachine']; + } + + if (isset($arguments['event'])) { + $syliusOptions['event'] = $arguments['event']; + } + + if (isset($arguments['returnContent'])) { + $syliusOptions['return_content'] = $arguments['returnContent']; + } + $route = new Route( $arguments['path'], [ diff --git a/src/Bundle/Tests/Routing/RoutesAttributesLoaderTest.php b/src/Bundle/Tests/Routing/RoutesAttributesLoaderTest.php index 898efa972..868664e75 100644 --- a/src/Bundle/Tests/Routing/RoutesAttributesLoaderTest.php +++ b/src/Bundle/Tests/Routing/RoutesAttributesLoaderTest.php @@ -19,9 +19,7 @@ final class RoutesAttributesLoaderTest extends KernelTestCase { - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -41,9 +39,7 @@ public function it_generates_routes_from_resource(): void ], $route->getDefaults()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_methods(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -64,9 +60,7 @@ public function it_generates_routes_from_resource_with_methods(): void ], $route->getDefaults()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_criteria(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -90,9 +84,7 @@ public function it_generates_routes_from_resource_with_criteria(): void ], $route->getDefaults()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_template(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -114,9 +106,7 @@ public function it_generates_routes_from_resource_with_template(): void ], $route->getDefaults()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_repository(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -141,9 +131,7 @@ public function it_generates_routes_from_resource_with_repository(): void ], $route->getDefaults()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_serialization_groups(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -165,9 +153,7 @@ public function it_generates_routes_from_resource_with_serialization_groups(): v ], $route->getDefaults()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_serialization_version(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -189,9 +175,7 @@ public function it_generates_routes_from_resource_with_serialization_version(): ], $route->getDefaults()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_vars(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -215,9 +199,7 @@ public function it_generates_routes_from_resource_with_vars(): void ], $route->getDefaults()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_requirements(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -236,9 +218,7 @@ public function it_generates_routes_from_resource_with_requirements(): void ], $route->getRequirements()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_priority(): void { if (Kernel::MAJOR_VERSION < 5) { @@ -258,9 +238,7 @@ public function it_generates_routes_from_resource_with_priority(): void $this->assertSame($route, array_values($routesCollection->all())[0]); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_options(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -280,9 +258,7 @@ public function it_generates_routes_from_resource_with_options(): void ], $route->getOptions()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_host(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -299,9 +275,7 @@ public function it_generates_routes_from_resource_with_host(): void $this->assertEquals('m.example.com', $route->getHost()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_schemes(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -318,9 +292,7 @@ public function it_generates_routes_from_resource_with_schemes(): void $this->assertEquals(['https'], $route->getSchemes()); } - /** - * @test - */ + /** @test */ public function it_generates_routes_from_resource_with_form(): void { self::bootKernel(['environment' => 'test_with_attributes']); @@ -341,4 +313,235 @@ public function it_generates_routes_from_resource_with_form(): void ], ], $route->getDefaults()); } + + /** @test */ + public function it_generates_routes_from_resource_with_form_options(): void + { + self::bootKernel(['environment' => 'test_with_attributes']); + + $container = static::$container; + + $attributesLoader = $container->get('sylius.routing.loader.routes_attributes'); + + $routesCollection = $attributesLoader->__invoke(); + + $route = $routesCollection->get('register_user_with_form_options'); + $this->assertNotNull($route); + $this->assertEquals('/users/register', $route->getPath()); + $this->assertEquals([ + '_controller' => 'app.controller.user:createAction', + '_sylius' => [ + 'form' => [ + 'type' => 'App\Form\Type\RegisterType', + 'options' => [ + 'validation_groups' => ['sylius', 'my_custom_group'], + ], + ], + ], + ], $route->getDefaults()); + } + + /** @test */ + public function it_generates_routes_from_resource_with_section(): void + { + self::bootKernel(['environment' => 'test_with_attributes']); + + $container = static::$container; + + $attributesLoader = $container->get('sylius.routing.loader.routes_attributes'); + + $routesCollection = $attributesLoader->__invoke(); + + $route = $routesCollection->get('show_book_with_section'); + $this->assertNotNull($route); + $this->assertEquals('/books/{id}', $route->getPath()); + $this->assertEquals([ + '_controller' => 'app.controller.book::showAction', + '_sylius' => [ + 'section' => 'admin', + ], + ], $route->getDefaults()); + } + + /** @test */ + public function it_generates_routes_from_resource_with_permission(): void + { + self::bootKernel(['environment' => 'test_with_attributes']); + + $container = static::$container; + + $attributesLoader = $container->get('sylius.routing.loader.routes_attributes'); + + $routesCollection = $attributesLoader->__invoke(); + + $route = $routesCollection->get('show_book_with_permission'); + $this->assertNotNull($route); + $this->assertEquals('/books/{id}', $route->getPath()); + $this->assertEquals([ + '_controller' => 'app.controller.book::showAction', + '_sylius' => [ + 'permission' => true, + ], + ], $route->getDefaults()); + } + + /** @test */ + public function it_generates_routes_from_resource_with_grid(): void + { + self::bootKernel(['environment' => 'test_with_attributes']); + + $container = static::$container; + + $attributesLoader = $container->get('sylius.routing.loader.routes_attributes'); + + $routesCollection = $attributesLoader->__invoke(); + + $route = $routesCollection->get('show_book_with_grid'); + $this->assertNotNull($route); + $this->assertEquals('/books/{id}', $route->getPath()); + $this->assertEquals([ + '_controller' => 'app.controller.book::showAction', + '_sylius' => [ + 'grid' => 'book', + ], + ], $route->getDefaults()); + } + + /** @test */ + public function it_generates_routes_from_resource_with_csrf_protection(): void + { + self::bootKernel(['environment' => 'test_with_attributes']); + + $container = static::$container; + + $attributesLoader = $container->get('sylius.routing.loader.routes_attributes'); + + $routesCollection = $attributesLoader->__invoke(); + + $route = $routesCollection->get('show_book_with_csrf_protection'); + $this->assertNotNull($route); + $this->assertEquals('/books/{id}', $route->getPath()); + $this->assertEquals([ + '_controller' => 'app.controller.book::showAction', + '_sylius' => [ + 'csrf_protection' => false, + ], + ], $route->getDefaults()); + } + + /** @test */ + public function it_generates_routes_from_resource_with_redirect(): void + { + self::bootKernel(['environment' => 'test_with_attributes']); + + $container = static::$container; + + $attributesLoader = $container->get('sylius.routing.loader.routes_attributes'); + + $routesCollection = $attributesLoader->__invoke(); + + $route = $routesCollection->get('update_book_with_redirect'); + $this->assertNotNull($route); + $this->assertEquals('/books/{id}', $route->getPath()); + $this->assertEquals([ + '_controller' => 'app.controller.book::showAction', + '_sylius' => [ + 'redirect' => 'referer', + ], + ], $route->getDefaults()); + } + + /** @test */ + public function it_generates_routes_from_resource_with_redirect_options(): void + { + self::bootKernel(['environment' => 'test_with_attributes']); + + $container = static::$container; + + $attributesLoader = $container->get('sylius.routing.loader.routes_attributes'); + + $routesCollection = $attributesLoader->__invoke(); + + $route = $routesCollection->get('update_book_with_redirect_options'); + $this->assertNotNull($route); + $this->assertEquals('/genre/{genreId}/books/{id}', $route->getPath()); + $this->assertEquals([ + '_controller' => 'app.controller.book::updateAction', + '_sylius' => [ + 'redirect' => [ + 'route' => 'app_genre_show', + 'parameters' => ['id' => '$genreId'], + ], + ], + ], $route->getDefaults()); + } + + /** @test */ + public function it_generates_routes_from_resource_with_state_machine(): void + { + self::bootKernel(['environment' => 'test_with_attributes']); + + $container = static::$container; + + $attributesLoader = $container->get('sylius.routing.loader.routes_attributes'); + + $routesCollection = $attributesLoader->__invoke(); + + $route = $routesCollection->get('publish_book'); + $this->assertNotNull($route); + $this->assertEquals('/books/{id}', $route->getPath()); + $this->assertEquals([ + '_controller' => 'app.controller.book::applyStateMachineTransitionAction', + '_sylius' => [ + 'state_machine' => [ + 'graph' => 'app_book', + 'transition' => 'publish', + ], + ], + ], $route->getDefaults()); + } + + /** @test */ + public function it_generates_routes_from_resource_with_custom_event_name(): void + { + self::bootKernel(['environment' => 'test_with_attributes']); + + $container = static::$container; + + $attributesLoader = $container->get('sylius.routing.loader.routes_attributes'); + + $routesCollection = $attributesLoader->__invoke(); + + $route = $routesCollection->get('update_book_with_custom_event_name'); + $this->assertNotNull($route); + $this->assertEquals('/books/{id}', $route->getPath()); + $this->assertEquals([ + '_controller' => 'app.controller.book::updateAction', + '_sylius' => [ + 'event' => 'customer_update', + ], + ], $route->getDefaults()); + } + + /** @test */ + public function it_generates_routes_from_resource_with_return_content(): void + { + self::bootKernel(['environment' => 'test_with_attributes']); + + $container = static::$container; + + $attributesLoader = $container->get('sylius.routing.loader.routes_attributes'); + + $routesCollection = $attributesLoader->__invoke(); + + $route = $routesCollection->get('update_book_with_return_content'); + $this->assertNotNull($route); + $this->assertEquals('/books/{id}', $route->getPath()); + $this->assertEquals([ + '_controller' => 'app.controller.book::updateAction', + '_sylius' => [ + 'return_content' => true, + ], + ], $route->getDefaults()); + } } diff --git a/src/Bundle/spec/Routing/RoutesAttributesLoaderSpec.php b/src/Bundle/spec/Routing/RoutesAttributesLoaderSpec.php index b37f0a12b..0357410fd 100644 --- a/src/Bundle/spec/Routing/RoutesAttributesLoaderSpec.php +++ b/src/Bundle/spec/Routing/RoutesAttributesLoaderSpec.php @@ -39,7 +39,7 @@ function it_is_a_route_loader(): void function it_generates_routes_from_paths(RouteAttributesFactoryInterface $routeAttributesFactory): void { - $routeAttributesFactory->createRouteForClass(Argument::type(RouteCollection::class), Argument::type('string'))->shouldBeCalledTimes(14); + $routeAttributesFactory->createRouteForClass(Argument::type(RouteCollection::class), Argument::type('string'))->shouldBeCalledTimes(24); $this->__invoke(); } diff --git a/src/Bundle/test/src/Entity/Route/PublishBook.php b/src/Bundle/test/src/Entity/Route/PublishBook.php new file mode 100644 index 000000000..1fc19af62 --- /dev/null +++ b/src/Bundle/test/src/Entity/Route/PublishBook.php @@ -0,0 +1,32 @@ + 'app_book', 'transition' => 'publish'] +)] +class PublishBook extends Book +{ +} diff --git a/src/Bundle/test/src/Entity/Route/RegisterUserWithFormOptions.php b/src/Bundle/test/src/Entity/Route/RegisterUserWithFormOptions.php new file mode 100644 index 000000000..35cd894c7 --- /dev/null +++ b/src/Bundle/test/src/Entity/Route/RegisterUserWithFormOptions.php @@ -0,0 +1,34 @@ + RegisterType::class, + 'options' => [ + 'validation_groups' => ['sylius', 'my_custom_group'], + ], + ], +)] +final class RegisterUserWithFormOptions extends User +{ +} diff --git a/src/Bundle/test/src/Entity/Route/ShowBookWithCsrfProtection.php b/src/Bundle/test/src/Entity/Route/ShowBookWithCsrfProtection.php new file mode 100644 index 000000000..c2bd85f7c --- /dev/null +++ b/src/Bundle/test/src/Entity/Route/ShowBookWithCsrfProtection.php @@ -0,0 +1,28 @@ + 'app_genre_show', + 'parameters' => ['id' => '$genreId'], + ], +)] +final class UpdateBookWithRedirectOptions extends Book +{ +} diff --git a/src/Bundle/test/src/Entity/Route/UpdateBookWithReturnContent.php b/src/Bundle/test/src/Entity/Route/UpdateBookWithReturnContent.php new file mode 100644 index 000000000..3b2953068 --- /dev/null +++ b/src/Bundle/test/src/Entity/Route/UpdateBookWithReturnContent.php @@ -0,0 +1,28 @@ +name = $name; - $this->path = $path; - $this->methods = $methods; - $this->controller = $controller; - $this->template = $template; - $this->repository = $repository; - $this->criteria = $criteria; - $this->serializationGroups = $serializationGroups; - $this->serializationVersion = $serializationVersion; - $this->requirements = $requirements; - $this->options = $options; - $this->host = $host; - $this->schemes = $schemes; - $this->vars = $vars; - $this->priority = $priority; - $this->form = $form; } }