From 36d91fe8b734bc391733696f026e3249b5330b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Deruss=C3=A9?= Date: Sun, 23 Oct 2022 17:17:26 +0200 Subject: [PATCH 1/2] Add documentation about the Doctrine Entity Value Resolver --- .doctor-rst.yaml | 1 + best_practices.rst | 13 ++- doctrine.rst | 266 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 265 insertions(+), 15 deletions(-) diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index 7323d53d1dd..5606ed239a1 100644 --- a/.doctor-rst.yaml +++ b/.doctor-rst.yaml @@ -88,6 +88,7 @@ whitelist: - '.. versionadded:: 1.11' # Messenger (Middleware / DoctrineBundle) - '.. versionadded:: 1.18' # Flex in setup/upgrade_minor.rst - '.. versionadded:: 1.0.0' # Encore + - '.. versionadded:: 2.7.1' # Doctrine - '0 => 123' # assertion for var_dumper - components/var_dumper.rst - '1 => "foo"' # assertion for var_dumper - components/var_dumper.rst - '123,' # assertion for var_dumper - components/var_dumper.rst diff --git a/best_practices.rst b/best_practices.rst index bc2269b4236..efe15c1c6fa 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -246,16 +246,17 @@ Instead, you must use dependency injection to fetch services by :ref:`type-hinting action method arguments ` or constructor arguments. -Use ParamConverters If They Are Convenient -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Use EntityValueResolver If It Is Convenient +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you're using :doc:`Doctrine `, then you can *optionally* use the -`ParamConverter`_ to automatically query for an entity and pass it as an argument -to your controller. It will also show a 404 page if no entity can be found. +`EntityValueResolver`_ to automatically query for an entity and pass it as an +argument to your controller. It will also show a 404 page if no entity can be +found. If the logic to get an entity from a route variable is more complex, instead of -configuring the ParamConverter, it's better to make the Doctrine query inside -the controller (e.g. by calling to a :doc:`Doctrine repository method `). +configuring the EntityValueResolver, it's better to make the Doctrine query +inside the controller (e.g. by calling to a :doc:`Doctrine repository method `). Templates --------- diff --git a/doctrine.rst b/doctrine.rst index 632641d1f11..6ad5c6d665d 100644 --- a/doctrine.rst +++ b/doctrine.rst @@ -598,15 +598,59 @@ the :ref:`doctrine-queries` section. see the web debug toolbar, install the ``profiler`` :ref:`Symfony pack ` by running this command: ``composer require --dev symfony/profiler-pack``. -Automatically Fetching Objects (ParamConverter) ------------------------------------------------ +Automatically Fetching Objects (EntityValueResolver) +---------------------------------------------------- -In many cases, you can use the `SensioFrameworkExtraBundle`_ to do the query -for you automatically! First, install the bundle in case you don't have it: +In many cases, you can use the `EntityValueResolver`_ to do the query for you +automatically! First, enable the feature: -.. code-block:: terminal +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/doctrine.yaml + doctrine: + orm: + controller_resolver: + enabled: true + auto_mapping: true + evict_cache: false + + .. code-block:: xml + + + + - $ composer require sensio/framework-extra-bundle + + + + + + + + + + .. code-block:: php + + // config/packages/doctrine.php + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine) { + $controllerResolver = $doctrine->orm() + ->entityManager('default') + // ... + ->controllerResolver(); + + $controllerResolver->autoMapping(true); + $controllerResolver->evictCache(true); + }; Now, simplify your controller:: @@ -621,7 +665,7 @@ Now, simplify your controller:: class ProductController extends AbstractController { - #[Route('/product/{id}', name: 'product_show')] + #[Route('/product/{id}')] public function show(Product $product): Response { // use the Product! @@ -632,7 +676,212 @@ Now, simplify your controller:: That's it! The bundle uses the ``{id}`` from the route to query for the ``Product`` by the ``id`` column. If it's not found, a 404 page is generated. -There are many more options you can use. Read more about the `ParamConverter`_. +This behavior can be enabled on all your controllers, by setting the ``auto_mapping`` +parameter to ``true``. Or individually on the desired controllers by using the +``MapEntity`` attribute: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use App\Repository\ProductRepository; + use Symfony\Bridge\Doctrine\Attribute\MapEntity; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; + // ... + + class ProductController extends AbstractController + { + #[Route('/product/{id}')] + public function show( + #[MapEntity] + Product $product + ): Response { + // use the Product! + // ... + } + } + +Fetch Automatically +~~~~~~~~~~~~~~~~~~~ + +If your route wildcards match properties on your entity, then the resolver +will automatically fetch them: + +.. configuration-block:: + + .. code-block:: php-attributes + + /** + * Fetch via primary key because {id} is in the route. + */ + #[Route('/product/{id}')] + public function showByPk(Post $post): Response + { + } + + /** + * Perform a findOneBy() where the slug property matches {slug}. + */ + #[Route('/product/{slug}')] + public function showBySlug(Post $post): Response + { + } + +Automatic fetching works in these situations: + +* If ``{id}`` is in your route, then this is used to fetch by + primary key via the ``find()`` method. + +* The resolver will attempt to do a ``findOneBy()`` fetch by using + *all* of the wildcards in your route that are actually properties + on your entity (non-properties are ignored). + +You can control this behavior by actually *adding* the ``MapEntity`` +attribute and using the `MapEntity options`_. + +Fetch via an Expression +~~~~~~~~~~~~~~~~~~~~~~~ + +If automatic fetching doesn't work, use an expression: + +.. configuration-block:: + + .. code-block:: php-attributes + + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; + + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(expr: 'repository.find(product_id)')] + Product $product + ): Response { + } + +Use the special ``MapEntity`` attribute with an ``expr`` option to +fetch the object by calling a method on your repository. The +``repository`` method will be your entity's Repository class and +any route wildcards - like ``{product_id}`` are available as variables. + +This can also be used to help resolve multiple arguments: + +.. configuration-block:: + + .. code-block:: php-attributes + + #[Route('/product/{id}/comments/{comment_id}')] + public function show( + Product $product + #[MapEntity(expr: 'repository.find(comment_id)')] + Comment $comment + ): Response { + } + +In the example above, the ``$product`` argument is handled automatically, +but ``$comment`` is configured with the attribute since they cannot both follow +the default convention. + +.. _`MapEntity options`: + + +MapEntity Options +~~~~~~~~~~~~~~~~~ + +A number of ``options`` are available on the ``MapEntity`` annotation to +control behavior: + +* ``id``: If an ``id`` option is configured and matches a route parameter, then + the resolver will find by the primary key: + + .. configuration-block:: + + .. code-block:: php-attributes + + #[Route('/product/{product_id}')] + public function show( + Product $product + #[MapEntity(id: 'product_id')] + Comment $comment + ): Response { + } + +* ``mapping``: Configures the properties and values to use with the ``findOneBy()`` + method: the key is the route placeholder name and the value is the Doctrine + property name: + + .. configuration-block:: + + .. code-block:: php-attributes + + #[Route('/product/{category}/{slug}/comments/{comment_slug}')] + public function show( + #[MapEntity(mapping: ['date' => 'date', 'slug' => 'slug'])] + Product $product + #[MapEntity(mapping: ['comment_slug' => 'slug'])] + Comment $comment + ): Response { + } + +* ``exclude`` Configures the properties that should be used in the ``findOneBy()`` + method by *excluding* one or more properties so that not *all* are used: + + .. configuration-block:: + + .. code-block:: php-attributes + + #[Route('/product/{slug}/{date}')] + public function show( + #[MapEntity(exclude: ['date'])] + Product $product + \DateTime $date + ): Response { + } + +* ``stripNull`` If true, then when ``findOneBy()`` is used, any values that + are ``null`` will not be used for the query. + +* ``entityManager`` By default, the ``EntityValueResolver`` uses the *default* + entity manager, but you can configure this: + + .. configuration-block:: + + .. code-block:: php-attributes + + #[Route('/product/{id}')] + public function show( + #[MapEntity(entityManager: ['foo'])] + Product $product + ): Response { + } + +* ``evictCache`` If true, forces Doctrine to always fetch the entity from the + database instead of cache. + +* ``disabled`` If true, the ``EntityValueResolver`` will not try to replace + the argument. + +.. tip:: + + When enabled globally, it's possible to disabled the behavior on a specific + controller, by using the ``MapEntity`` set to ``disabled``. + + public function show( + #[CurrentUser] + #[MapEntity(disabled: true)] + User $user + ): Response { + // User is not resolved by the EntityValueResolver + // ... + } + +.. versionadded:: 6.2 + + Entity Value Resolver was introduced in Symfony 6.2. + +.. versionadded:: 2.7.1 + + Autowiring of the ``EntityValueResolver`` was introduced in DoctrineBundle + 2.7.1. Updating an Object ------------------ @@ -896,7 +1145,6 @@ Learn more .. _`DoctrineMigrationsBundle`: https://github.com/doctrine/DoctrineMigrationsBundle .. _`NativeQuery`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/native-sql.html .. _`SensioFrameworkExtraBundle`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html -.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html .. _`limit of 767 bytes for the index key prefix`: https://dev.mysql.com/doc/refman/5.6/en/innodb-limits.html .. _`Doctrine screencast series`: https://symfonycasts.com/screencast/symfony-doctrine .. _`API Platform`: https://api-platform.com/docs/core/validation/ From dc7c6a1366c0c87c17b707256b61f6e5eb9c821f Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Tue, 6 Dec 2022 15:46:06 +0100 Subject: [PATCH 2/2] [#17393] Finish entity value resolver docs --- best_practices.rst | 13 +- doctrine.rst | 294 +++++++++++++++++---------------------------- 2 files changed, 116 insertions(+), 191 deletions(-) diff --git a/best_practices.rst b/best_practices.rst index efe15c1c6fa..26187fd4d17 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -246,13 +246,13 @@ Instead, you must use dependency injection to fetch services by :ref:`type-hinting action method arguments ` or constructor arguments. -Use EntityValueResolver If It Is Convenient -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Use Entity Value Resolvers If They Are Convenient +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you're using :doc:`Doctrine `, then you can *optionally* use the -`EntityValueResolver`_ to automatically query for an entity and pass it as an -argument to your controller. It will also show a 404 page if no entity can be -found. +If you're using :doc:`Doctrine `, then you can *optionally* use +the :ref:`EntityValueResolver ` to +automatically query for an entity and pass it as an argument to your +controller. It will also show a 404 page if no entity can be found. If the logic to get an entity from a route variable is more complex, instead of configuring the EntityValueResolver, it's better to make the Doctrine query @@ -451,7 +451,6 @@ you must set up a redirection. .. _`Symfony Demo`: https://github.com/symfony/demo .. _`download Symfony`: https://symfony.com/download .. _`Composer`: https://getcomposer.org/ -.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html .. _`feature toggles`: https://en.wikipedia.org/wiki/Feature_toggle .. _`smoke testing`: https://en.wikipedia.org/wiki/Smoke_testing_(software) .. _`Webpack`: https://webpack.js.org/ diff --git a/doctrine.rst b/doctrine.rst index 6ad5c6d665d..9e100a5e00b 100644 --- a/doctrine.rst +++ b/doctrine.rst @@ -598,61 +598,21 @@ the :ref:`doctrine-queries` section. see the web debug toolbar, install the ``profiler`` :ref:`Symfony pack ` by running this command: ``composer require --dev symfony/profiler-pack``. +.. _doctrine-entity-value-resolver: + Automatically Fetching Objects (EntityValueResolver) ---------------------------------------------------- -In many cases, you can use the `EntityValueResolver`_ to do the query for you -automatically! First, enable the feature: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/doctrine.yaml - doctrine: - orm: - controller_resolver: - enabled: true - auto_mapping: true - evict_cache: false - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php +.. versionadded:: 6.2 - // config/packages/doctrine.php - use Symfony\Config\DoctrineConfig; + Entity Value Resolver was introduced in Symfony 6.2. - return static function (DoctrineConfig $doctrine) { - $controllerResolver = $doctrine->orm() - ->entityManager('default') - // ... - ->controllerResolver(); +.. versionadded:: 2.7.1 - $controllerResolver->autoMapping(true); - $controllerResolver->evictCache(true); - }; + Autowiring of the ``EntityValueResolver`` was introduced in DoctrineBundle 2.7.1. -Now, simplify your controller:: +In many cases, you can use the ``EntityValueResolver`` to do the query for you +automatically! You can simplify the controller to:: // src/Controller/ProductController.php namespace App\Controller; @@ -676,15 +636,17 @@ Now, simplify your controller:: That's it! The bundle uses the ``{id}`` from the route to query for the ``Product`` by the ``id`` column. If it's not found, a 404 page is generated. -This behavior can be enabled on all your controllers, by setting the ``auto_mapping`` -parameter to ``true``. Or individually on the desired controllers by using the -``MapEntity`` attribute: +This behavior is enabled by default on all your controllers. You can +disable it by setting the ``doctrine.orm.controller_resolver.auto_mapping`` +config option to ``false``. + +When disabled, you can enable it individually on the desired controllers by +using the ``MapEntity`` attribute:: // src/Controller/ProductController.php namespace App\Controller; use App\Entity\Product; - use App\Repository\ProductRepository; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -702,31 +664,41 @@ parameter to ``true``. Or individually on the desired controllers by using the } } +.. tip:: + + When enabled globally, it's possible to disabled the behavior on a specific + controller, by using the ``MapEntity`` set to ``disabled``. + + public function show( + #[CurrentUser] + #[MapEntity(disabled: true)] + User $user + ): Response { + // User is not resolved by the EntityValueResolver + // ... + } + Fetch Automatically ~~~~~~~~~~~~~~~~~~~ If your route wildcards match properties on your entity, then the resolver -will automatically fetch them: - -.. configuration-block:: - - .. code-block:: php-attributes +will automatically fetch them:: - /** - * Fetch via primary key because {id} is in the route. - */ - #[Route('/product/{id}')] - public function showByPk(Post $post): Response - { - } + /** + * Fetch via primary key because {id} is in the route. + */ + #[Route('/product/{id}')] + public function showByPk(Post $post): Response + { + } - /** - * Perform a findOneBy() where the slug property matches {slug}. - */ - #[Route('/product/{slug}')] - public function showBySlug(Post $post): Response - { - } + /** + * Perform a findOneBy() where the slug property matches {slug}. + */ + #[Route('/product/{slug}')] + public function showBySlug(Post $post): Response + { + } Automatic fetching works in these situations: @@ -743,145 +715,99 @@ attribute and using the `MapEntity options`_. Fetch via an Expression ~~~~~~~~~~~~~~~~~~~~~~~ -If automatic fetching doesn't work, use an expression: - -.. configuration-block:: - - .. code-block:: php-attributes +If automatic fetching doesn't work, you can write an expression using the +:doc:`ExpressionLanguage component `:: - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; - - #[Route('/product/{product_id}')] - public function show( - #[MapEntity(expr: 'repository.find(product_id)')] - Product $product - ): Response { - } - -Use the special ``MapEntity`` attribute with an ``expr`` option to -fetch the object by calling a method on your repository. The -``repository`` method will be your entity's Repository class and -any route wildcards - like ``{product_id}`` are available as variables. - -This can also be used to help resolve multiple arguments: + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(expr: 'repository.find(product_id)')] + Product $product + ): Response { + } -.. configuration-block:: +In the expression, the ``repository`` variable will be your entity's +Repository class and any route wildcards - like ``{product_id}`` are +available as variables. - .. code-block:: php-attributes +This can also be used to help resolve multiple arguments:: - #[Route('/product/{id}/comments/{comment_id}')] - public function show( - Product $product - #[MapEntity(expr: 'repository.find(comment_id)')] - Comment $comment - ): Response { - } + #[Route('/product/{id}/comments/{comment_id}')] + public function show( + Product $product + #[MapEntity(expr: 'repository.find(comment_id)')] + Comment $comment + ): Response { + } -In the example above, the ``$product`` argument is handled automatically, +In the example above, the ``$product`` argument is handled automatically, but ``$comment`` is configured with the attribute since they cannot both follow the default convention. -.. _`MapEntity options`: - - MapEntity Options ~~~~~~~~~~~~~~~~~ -A number of ``options`` are available on the ``MapEntity`` annotation to +A number of options are available on the ``MapEntity`` annotation to control behavior: -* ``id``: If an ``id`` option is configured and matches a route parameter, then - the resolver will find by the primary key: - - .. configuration-block:: - - .. code-block:: php-attributes - - #[Route('/product/{product_id}')] - public function show( - Product $product - #[MapEntity(id: 'product_id')] - Comment $comment - ): Response { - } - -* ``mapping``: Configures the properties and values to use with the ``findOneBy()`` - method: the key is the route placeholder name and the value is the Doctrine - property name: - - .. configuration-block:: - - .. code-block:: php-attributes - - #[Route('/product/{category}/{slug}/comments/{comment_slug}')] - public function show( - #[MapEntity(mapping: ['date' => 'date', 'slug' => 'slug'])] - Product $product - #[MapEntity(mapping: ['comment_slug' => 'slug'])] - Comment $comment - ): Response { - } - -* ``exclude`` Configures the properties that should be used in the ``findOneBy()`` - method by *excluding* one or more properties so that not *all* are used: - - .. configuration-block:: - - .. code-block:: php-attributes - - #[Route('/product/{slug}/{date}')] - public function show( - #[MapEntity(exclude: ['date'])] - Product $product - \DateTime $date - ): Response { - } - -* ``stripNull`` If true, then when ``findOneBy()`` is used, any values that - are ``null`` will not be used for the query. - -* ``entityManager`` By default, the ``EntityValueResolver`` uses the *default* - entity manager, but you can configure this: +``id`` + If an ``id`` option is configured and matches a route parameter, then + the resolver will find by the primary key:: - .. configuration-block:: + #[Route('/product/{product_id}')] + public function show( + Product $product + #[MapEntity(id: 'product_id')] + Comment $comment + ): Response { + } - .. code-block:: php-attributes +``mapping`` + Configures the properties and values to use with the ``findOneBy()`` + method: the key is the route placeholder name and the value is the Doctrine + property name:: - #[Route('/product/{id}')] - public function show( - #[MapEntity(entityManager: ['foo'])] - Product $product - ): Response { - } + #[Route('/product/{category}/{slug}/comments/{comment_slug}')] + public function show( + #[MapEntity(mapping: ['date' => 'date', 'slug' => 'slug'])] + Product $product + #[MapEntity(mapping: ['comment_slug' => 'slug'])] + Comment $comment + ): Response { + } -* ``evictCache`` If true, forces Doctrine to always fetch the entity from the - database instead of cache. +``exclude`` + Configures the properties that should be used in the ``findOneBy()`` + method by *excluding* one or more properties so that not *all* are used: -* ``disabled`` If true, the ``EntityValueResolver`` will not try to replace - the argument. + #[Route('/product/{slug}/{date}')] + public function show( + #[MapEntity(exclude: ['date'])] + Product $product + \DateTime $date + ): Response { + } -.. tip:: +``stripNull`` + If true, then when ``findOneBy()`` is used, any values that are + ``null`` will not be used for the query. - When enabled globally, it's possible to disabled the behavior on a specific - controller, by using the ``MapEntity`` set to ``disabled``. +``entityManager`` + By default, the ``EntityValueResolver`` uses the *default* + entity manager, but you can configure this:: + #[Route('/product/{id}')] public function show( - #[CurrentUser] - #[MapEntity(disabled: true)] - User $user + #[MapEntity(entityManager: ['foo'])] + Product $product ): Response { - // User is not resolved by the EntityValueResolver - // ... } -.. versionadded:: 6.2 - - Entity Value Resolver was introduced in Symfony 6.2. - -.. versionadded:: 2.7.1 +``evictCache`` + If true, forces Doctrine to always fetch the entity from the database + instead of cache. - Autowiring of the ``EntityValueResolver`` was introduced in DoctrineBundle - 2.7.1. +``disabled`` + If true, the ``EntityValueResolver`` will not try to replace the argument. Updating an Object ------------------