Skip to content

Commit 36d91fe

Browse files
committed
Add documentation about the Doctrine Entity Value Resolver
1 parent 1544a18 commit 36d91fe

File tree

3 files changed

+265
-15
lines changed

3 files changed

+265
-15
lines changed

.doctor-rst.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ whitelist:
8888
- '.. versionadded:: 1.11' # Messenger (Middleware / DoctrineBundle)
8989
- '.. versionadded:: 1.18' # Flex in setup/upgrade_minor.rst
9090
- '.. versionadded:: 1.0.0' # Encore
91+
- '.. versionadded:: 2.7.1' # Doctrine
9192
- '0 => 123' # assertion for var_dumper - components/var_dumper.rst
9293
- '1 => "foo"' # assertion for var_dumper - components/var_dumper.rst
9394
- '123,' # assertion for var_dumper - components/var_dumper.rst

best_practices.rst

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,16 +246,17 @@ Instead, you must use dependency injection to fetch services by
246246
:ref:`type-hinting action method arguments <controller-accessing-services>` or
247247
constructor arguments.
248248

249-
Use ParamConverters If They Are Convenient
250-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
249+
Use EntityValueResolver If It Is Convenient
250+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
251251

252252
If you're using :doc:`Doctrine </doctrine>`, then you can *optionally* use the
253-
`ParamConverter`_ to automatically query for an entity and pass it as an argument
254-
to your controller. It will also show a 404 page if no entity can be found.
253+
`EntityValueResolver`_ to automatically query for an entity and pass it as an
254+
argument to your controller. It will also show a 404 page if no entity can be
255+
found.
255256

256257
If the logic to get an entity from a route variable is more complex, instead of
257-
configuring the ParamConverter, it's better to make the Doctrine query inside
258-
the controller (e.g. by calling to a :doc:`Doctrine repository method </doctrine>`).
258+
configuring the EntityValueResolver, it's better to make the Doctrine query
259+
inside the controller (e.g. by calling to a :doc:`Doctrine repository method </doctrine>`).
259260

260261
Templates
261262
---------

doctrine.rst

Lines changed: 257 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -598,15 +598,59 @@ the :ref:`doctrine-queries` section.
598598
see the web debug toolbar, install the ``profiler`` :ref:`Symfony pack <symfony-packs>`
599599
by running this command: ``composer require --dev symfony/profiler-pack``.
600600

601-
Automatically Fetching Objects (ParamConverter)
602-
-----------------------------------------------
601+
Automatically Fetching Objects (EntityValueResolver)
602+
----------------------------------------------------
603603

604-
In many cases, you can use the `SensioFrameworkExtraBundle`_ to do the query
605-
for you automatically! First, install the bundle in case you don't have it:
604+
In many cases, you can use the `EntityValueResolver`_ to do the query for you
605+
automatically! First, enable the feature:
606606

607-
.. code-block:: terminal
607+
.. configuration-block::
608+
609+
.. code-block:: yaml
610+
611+
# config/packages/doctrine.yaml
612+
doctrine:
613+
orm:
614+
controller_resolver:
615+
enabled: true
616+
auto_mapping: true
617+
evict_cache: false
618+
619+
.. code-block:: xml
620+
621+
<!-- config/packages/doctrine.xml -->
622+
<?xml version="1.0" encoding="UTF-8" ?>
623+
<container xmlns="http://symfony.com/schema/dic/services"
624+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
625+
xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
626+
xsi:schemaLocation="http://symfony.com/schema/dic/services
627+
https://symfony.com/schema/dic/services/services-1.0.xsd
628+
http://symfony.com/schema/dic/doctrine
629+
https://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">
608630
609-
$ composer require sensio/framework-extra-bundle
631+
<doctrine:config>
632+
<!-- by convention the env var names are always uppercase -->
633+
<doctrine:orm>
634+
<doctrine:controller_resolver auto_mapping="true" evict_cache="false"/>
635+
</doctrine:orm>
636+
</doctrine:config>
637+
638+
</container>
639+
640+
.. code-block:: php
641+
642+
// config/packages/doctrine.php
643+
use Symfony\Config\DoctrineConfig;
644+
645+
return static function (DoctrineConfig $doctrine) {
646+
$controllerResolver = $doctrine->orm()
647+
->entityManager('default')
648+
// ...
649+
->controllerResolver();
650+
651+
$controllerResolver->autoMapping(true);
652+
$controllerResolver->evictCache(true);
653+
};
610654
611655
Now, simplify your controller::
612656

@@ -621,7 +665,7 @@ Now, simplify your controller::
621665

622666
class ProductController extends AbstractController
623667
{
624-
#[Route('/product/{id}', name: 'product_show')]
668+
#[Route('/product/{id}')]
625669
public function show(Product $product): Response
626670
{
627671
// use the Product!
@@ -632,7 +676,212 @@ Now, simplify your controller::
632676
That's it! The bundle uses the ``{id}`` from the route to query for the ``Product``
633677
by the ``id`` column. If it's not found, a 404 page is generated.
634678

635-
There are many more options you can use. Read more about the `ParamConverter`_.
679+
This behavior can be enabled on all your controllers, by setting the ``auto_mapping``
680+
parameter to ``true``. Or individually on the desired controllers by using the
681+
``MapEntity`` attribute:
682+
683+
// src/Controller/ProductController.php
684+
namespace App\Controller;
685+
686+
use App\Entity\Product;
687+
use App\Repository\ProductRepository;
688+
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
689+
use Symfony\Component\HttpFoundation\Response;
690+
use Symfony\Component\Routing\Annotation\Route;
691+
// ...
692+
693+
class ProductController extends AbstractController
694+
{
695+
#[Route('/product/{id}')]
696+
public function show(
697+
#[MapEntity]
698+
Product $product
699+
): Response {
700+
// use the Product!
701+
// ...
702+
}
703+
}
704+
705+
Fetch Automatically
706+
~~~~~~~~~~~~~~~~~~~
707+
708+
If your route wildcards match properties on your entity, then the resolver
709+
will automatically fetch them:
710+
711+
.. configuration-block::
712+
713+
.. code-block:: php-attributes
714+
715+
/**
716+
* Fetch via primary key because {id} is in the route.
717+
*/
718+
#[Route('/product/{id}')]
719+
public function showByPk(Post $post): Response
720+
{
721+
}
722+
723+
/**
724+
* Perform a findOneBy() where the slug property matches {slug}.
725+
*/
726+
#[Route('/product/{slug}')]
727+
public function showBySlug(Post $post): Response
728+
{
729+
}
730+
731+
Automatic fetching works in these situations:
732+
733+
* If ``{id}`` is in your route, then this is used to fetch by
734+
primary key via the ``find()`` method.
735+
736+
* The resolver will attempt to do a ``findOneBy()`` fetch by using
737+
*all* of the wildcards in your route that are actually properties
738+
on your entity (non-properties are ignored).
739+
740+
You can control this behavior by actually *adding* the ``MapEntity``
741+
attribute and using the `MapEntity options`_.
742+
743+
Fetch via an Expression
744+
~~~~~~~~~~~~~~~~~~~~~~~
745+
746+
If automatic fetching doesn't work, use an expression:
747+
748+
.. configuration-block::
749+
750+
.. code-block:: php-attributes
751+
752+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
753+
754+
#[Route('/product/{product_id}')]
755+
public function show(
756+
#[MapEntity(expr: 'repository.find(product_id)')]
757+
Product $product
758+
): Response {
759+
}
760+
761+
Use the special ``MapEntity`` attribute with an ``expr`` option to
762+
fetch the object by calling a method on your repository. The
763+
``repository`` method will be your entity's Repository class and
764+
any route wildcards - like ``{product_id}`` are available as variables.
765+
766+
This can also be used to help resolve multiple arguments:
767+
768+
.. configuration-block::
769+
770+
.. code-block:: php-attributes
771+
772+
#[Route('/product/{id}/comments/{comment_id}')]
773+
public function show(
774+
Product $product
775+
#[MapEntity(expr: 'repository.find(comment_id)')]
776+
Comment $comment
777+
): Response {
778+
}
779+
780+
In the example above, the ``$product`` argument is handled automatically,
781+
but ``$comment`` is configured with the attribute since they cannot both follow
782+
the default convention.
783+
784+
.. _`MapEntity options`:
785+
786+
787+
MapEntity Options
788+
~~~~~~~~~~~~~~~~~
789+
790+
A number of ``options`` are available on the ``MapEntity`` annotation to
791+
control behavior:
792+
793+
* ``id``: If an ``id`` option is configured and matches a route parameter, then
794+
the resolver will find by the primary key:
795+
796+
.. configuration-block::
797+
798+
.. code-block:: php-attributes
799+
800+
#[Route('/product/{product_id}')]
801+
public function show(
802+
Product $product
803+
#[MapEntity(id: 'product_id')]
804+
Comment $comment
805+
): Response {
806+
}
807+
808+
* ``mapping``: Configures the properties and values to use with the ``findOneBy()``
809+
method: the key is the route placeholder name and the value is the Doctrine
810+
property name:
811+
812+
.. configuration-block::
813+
814+
.. code-block:: php-attributes
815+
816+
#[Route('/product/{category}/{slug}/comments/{comment_slug}')]
817+
public function show(
818+
#[MapEntity(mapping: ['date' => 'date', 'slug' => 'slug'])]
819+
Product $product
820+
#[MapEntity(mapping: ['comment_slug' => 'slug'])]
821+
Comment $comment
822+
): Response {
823+
}
824+
825+
* ``exclude`` Configures the properties that should be used in the ``findOneBy()``
826+
method by *excluding* one or more properties so that not *all* are used:
827+
828+
.. configuration-block::
829+
830+
.. code-block:: php-attributes
831+
832+
#[Route('/product/{slug}/{date}')]
833+
public function show(
834+
#[MapEntity(exclude: ['date'])]
835+
Product $product
836+
\DateTime $date
837+
): Response {
838+
}
839+
840+
* ``stripNull`` If true, then when ``findOneBy()`` is used, any values that
841+
are ``null`` will not be used for the query.
842+
843+
* ``entityManager`` By default, the ``EntityValueResolver`` uses the *default*
844+
entity manager, but you can configure this:
845+
846+
.. configuration-block::
847+
848+
.. code-block:: php-attributes
849+
850+
#[Route('/product/{id}')]
851+
public function show(
852+
#[MapEntity(entityManager: ['foo'])]
853+
Product $product
854+
): Response {
855+
}
856+
857+
* ``evictCache`` If true, forces Doctrine to always fetch the entity from the
858+
database instead of cache.
859+
860+
* ``disabled`` If true, the ``EntityValueResolver`` will not try to replace
861+
the argument.
862+
863+
.. tip::
864+
865+
When enabled globally, it's possible to disabled the behavior on a specific
866+
controller, by using the ``MapEntity`` set to ``disabled``.
867+
868+
public function show(
869+
#[CurrentUser]
870+
#[MapEntity(disabled: true)]
871+
User $user
872+
): Response {
873+
// User is not resolved by the EntityValueResolver
874+
// ...
875+
}
876+
877+
.. versionadded:: 6.2
878+
879+
Entity Value Resolver was introduced in Symfony 6.2.
880+
881+
.. versionadded:: 2.7.1
882+
883+
Autowiring of the ``EntityValueResolver`` was introduced in DoctrineBundle
884+
2.7.1.
636885

637886
Updating an Object
638887
------------------
@@ -896,7 +1145,6 @@ Learn more
8961145
.. _`DoctrineMigrationsBundle`: https://github.com/doctrine/DoctrineMigrationsBundle
8971146
.. _`NativeQuery`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/native-sql.html
8981147
.. _`SensioFrameworkExtraBundle`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html
899-
.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
9001148
.. _`limit of 767 bytes for the index key prefix`: https://dev.mysql.com/doc/refman/5.6/en/innodb-limits.html
9011149
.. _`Doctrine screencast series`: https://symfonycasts.com/screencast/symfony-doctrine
9021150
.. _`API Platform`: https://api-platform.com/docs/core/validation/

0 commit comments

Comments
 (0)