From adadb5a76253fac2cbffa1ee1c7d2a2954894631 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Sun, 21 May 2017 11:02:13 -0400 Subject: [PATCH 1/2] updating best practices for DI 3.3 changes --- best_practices/business-logic.rst | 43 +++++++++++++++++++++++-------- best_practices/controllers.rst | 26 ++++++++++++++----- best_practices/security.rst | 7 ++--- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/best_practices/business-logic.rst b/best_practices/business-logic.rst index 21f066dbe49..4c62c6871ad 100644 --- a/best_practices/business-logic.rst +++ b/best_practices/business-logic.rst @@ -84,30 +84,41 @@ Next, define a new service for that class. # app/config/services.yml services: - # keep your service names short - app.slugger: - class: AppBundle\Utils\Slugger + # ... + + # use the class name as the service id + AppBundle\Utils\Slugger: + public: false + +.. note:: -Traditionally, the naming convention for a service involved following the -class name and location to avoid name collisions. Thus, the service -*would have been* called ``app.utils.slugger``. But by using short service names, -your code will be easier to read and use. + If you're using the :ref:`default services.yml configuration `, + the class is auto-registered as a service. + +Traditionally, the naming convention for a service was a short, but unique +snake case key - e.g. ``app.utils.slugger``. But for most services, you should now +use the class name. .. best-practice:: - The name of your application's services should be as short as possible, - but unique enough that you can search your project for the service if - you ever need to. + The id of your application's services should be equal to their class name, + except when you have multiple services configured for the same class (in that + case, use a snake case id). Now you can use the custom slugger in any controller class, such as the ``AdminController``: .. code-block:: php - public function createAction(Request $request) + use AppBundle\Utils\Slugger; + + public function createAction(Request $request, Slugger $slugger) { // ... + // you can also fetch a public service like this, but is not the best-practice + // $slugger = $this->get('app.slugger'); + if ($form->isSubmitted() && $form->isValid()) { $slug = $this->get('app.slugger')->slugify($post->getTitle()); $post->setSlug($slug); @@ -116,6 +127,16 @@ Now you can use the custom slugger in any controller class, such as the } } +Services can also be :ref:`public or private `. If you use the +:ref:`default services.yml configuration `, +all services are private by default. + +.. best-practice:: + + Services should be ``private`` whenever possible. This will prevent you from + accessing that service via ``$container->get()``. Instead, you will need to use + dependency injection. + Service Format: YAML -------------------- diff --git a/best_practices/controllers.rst b/best_practices/controllers.rst index 98e55f4eef7..da9fb088cf6 100644 --- a/best_practices/controllers.rst +++ b/best_practices/controllers.rst @@ -97,16 +97,16 @@ for the homepage of our app: use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; + use Doctrine\ORM\EntityManagerInterface; class DefaultController extends Controller { /** * @Route("/", name="homepage") */ - public function indexAction() + public function indexAction(EntityManagerInterface $em) { - $posts = $this->getDoctrine() - ->getRepository('AppBundle:Post') + $posts = $em->getRepository('AppBundle:Post') ->findLatest(); return $this->render('default/index.html.twig', array( @@ -115,6 +115,19 @@ for the homepage of our app: } } +Fetching Services +----------------- + +If you extend the base ``Controller`` class, you can access services directly from +the container via ``$this->container->get()`` or ``$this->get()``. Instead, you +should use dependency injection to fetch services: most easily done by +:ref:`type-hinting action method arguments `: + +.. best-practice:: + + Don't use ``$this->get()`` or ``$this->container->get()`` to fetch services + from the container. Instead, use dependency injection. + .. _best-practices-paramconverter: Using the ParamConverter @@ -164,13 +177,14 @@ manually. In our application, we have this situation in ``CommentController``: .. code-block:: php + use Doctrine\ORM\EntityManagerInterface; + /** * @Route("/comment/{postSlug}/new", name = "comment_new") */ - public function newAction(Request $request, $postSlug) + public function newAction(Request $request, $postSlug, EntityManagerInterface $em) { - $post = $this->getDoctrine() - ->getRepository('AppBundle:Post') + $post = $em->getRepository('AppBundle:Post') ->findOneBy(array('slug' => $postSlug)); if (!$post) { diff --git a/best_practices/security.rst b/best_practices/security.rst index 5252686d7f7..dcc9497e33c 100644 --- a/best_practices/security.rst +++ b/best_practices/security.rst @@ -224,9 +224,9 @@ more advanced use-case, you can always do the same security check in PHP: /** * @Route("/{id}/edit", name="admin_post_edit") */ - public function editAction($id) + public function editAction($id, EntityManagerInterface $em) { - $post = $this->getDoctrine()->getRepository('AppBundle:Post') + $post = $em->getRepository('AppBundle:Post') ->find($id); if (!$post) { @@ -328,7 +328,8 @@ the same ``getAuthorEmail()`` logic you used above: } } -Your application will :ref:`autoconfigure ` your security +If you're using the :ref:`default services.yml configuration `, +your application will :ref:`autoconfigure ` your security voter and inject a ``AccessDecisionManagerInterface`` instance in it thanks to :doc:`autowiring `. From 33647a6663659ea59b2144e0a8088238375babd9 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Sat, 27 May 2017 13:33:58 -0400 Subject: [PATCH 2/2] Tweaks after review! --- best_practices/business-logic.rst | 5 +++-- best_practices/controllers.rst | 12 +++++++----- service_container/alias_private.rst | 7 ++++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/best_practices/business-logic.rst b/best_practices/business-logic.rst index 4c62c6871ad..cb3f6e5413d 100644 --- a/best_practices/business-logic.rst +++ b/best_practices/business-logic.rst @@ -86,7 +86,7 @@ Next, define a new service for that class. services: # ... - # use the class name as the service id + # use the fully-qualified class name as the service id AppBundle\Utils\Slugger: public: false @@ -116,7 +116,8 @@ Now you can use the custom slugger in any controller class, such as the { // ... - // you can also fetch a public service like this, but is not the best-practice + // you can also fetch a public service like this + // but fetching services in this way is not considered a best practice // $slugger = $this->get('app.slugger'); if ($form->isSubmitted() && $form->isValid()) { diff --git a/best_practices/controllers.rst b/best_practices/controllers.rst index da9fb088cf6..89f6a497493 100644 --- a/best_practices/controllers.rst +++ b/best_practices/controllers.rst @@ -119,7 +119,7 @@ Fetching Services ----------------- If you extend the base ``Controller`` class, you can access services directly from -the container via ``$this->container->get()`` or ``$this->get()``. Instead, you +the container via ``$this->container->get()`` or ``$this->get()``. But instead, you should use dependency injection to fetch services: most easily done by :ref:`type-hinting action method arguments `: @@ -128,6 +128,9 @@ should use dependency injection to fetch services: most easily done by Don't use ``$this->get()`` or ``$this->container->get()`` to fetch services from the container. Instead, use dependency injection. +By not fetching services directly from the container, you can make your services +*private*, which has :ref:`several advantages `. + .. _best-practices-paramconverter: Using the ParamConverter @@ -177,14 +180,13 @@ manually. In our application, we have this situation in ``CommentController``: .. code-block:: php - use Doctrine\ORM\EntityManagerInterface; - /** * @Route("/comment/{postSlug}/new", name = "comment_new") */ - public function newAction(Request $request, $postSlug, EntityManagerInterface $em) + public function newAction(Request $request, $postSlug) { - $post = $em->getRepository('AppBundle:Post') + $post = $this->getDoctrine() + ->getRepository('AppBundle:Post') ->findOneBy(array('slug' => $postSlug)); if (!$post) { diff --git a/service_container/alias_private.rst b/service_container/alias_private.rst index b5fe9237dac..641b416861f 100644 --- a/service_container/alias_private.rst +++ b/service_container/alias_private.rst @@ -57,8 +57,13 @@ You can also control the ``public`` option on a service-by-service basis: $container->register(Foo::class) ->setPublic(false); +.. _services-why-private: + Private services are special because they allow the container to optimize whether -and how they are instantiated. This increases the container's performance. +and how they are instantiated. This increases the container's performance. It also +gives you better errors: if you try to reference a non-existent service, you will +get a clear error when you refresh *any* page, even if the problematic code would +not have run on that page. Now that the service is private, you *should not* fetch the service directly from the container::