Skip to content

Updated the Best Practices for Symfony 4 and Flex #8599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 25 additions & 68 deletions best_practices/business-logic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,25 @@ Inside here, you can create whatever directories you want to organize things:
├─ var/
└─ vendor/

Services: Naming and Format
---------------------------
.. _services-naming-and-format:

The blog application needs a utility that can transform a post title (e.g.
"Hello World") into a slug (e.g. "hello-world"). The slug will be used as
part of the post URL.
Services: Naming and Configuration
----------------------------------

.. best-practice::

Use autowiring to automate the configuration of application services.

Let's create a new ``Slugger`` class inside ``src/Utils/`` and
add the following ``slugify()`` method:
:doc:`Service autowiring </service_container/autowiring>` is a feature provided
by Symfony's Service Container to manage services with minimal configuration. It
reads the type-hints on your constructor (or other methods) and automatically
passes the correct services to each method. It can also add
:doc:`service tags </service_container/tags>` to the services needed them, such
as Twig extensions, event subscribers, etc.

The blog application needs a utility that can transform a post title (e.g.
"Hello World") into a slug (e.g. "hello-world") to include it as part of the
post URL. Let's create a new ``Slugger`` class inside ``src/Utils/``:

.. code-block:: php

Expand All @@ -42,56 +52,33 @@ add the following ``slugify()`` method:

class Slugger
{
public function slugify($string)
public function slugify(string $value): string
{
return preg_replace(
'/[^a-z0-9]/', '-', strtolower(trim(strip_tags($string)))
);
// ...
}
}

Next, define a new service for that class.

.. code-block:: yaml

# config/services.yaml
services:
# ...

# use the fully-qualified class name as the service id
App\Utils\Slugger:
public: false

.. note::

If you're using the :ref:`default services.yml configuration <service-container-services-load-example>`,
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.
If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`,
this class is auto-registered as a service whose ID is ``App\Utils\Slugger`` (or
simply ``Slugger::class`` if the class is already imported in your code).

.. best-practice::

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``:
Now you can use the custom slugger in any other service or controller class,
such as the ``AdminController``:

.. code-block:: php

use App\Utils\Slugger;

public function createAction(Request $request, Slugger $slugger)
public function create(Request $request, Slugger $slugger)
{
// ...

// you can also fetch a public service like this
// but fetching services in this way is not considered a best practice
// $slugger = $this->get(Slugger::class);

if ($form->isSubmitted() && $form->isValid()) {
$slug = $slugger->slugify($post->getTitle());
$post->setSlug($slug);
Expand Down Expand Up @@ -127,36 +114,6 @@ personal taste.
We recommend YAML because it's friendly to newcomers and concise. You can
of course use whatever format you like.

Service: No Class Parameter
---------------------------

You may have noticed that the previous service definition doesn't configure
the class namespace as a parameter:

.. code-block:: yaml

# config/services.yaml

# service definition with class namespace as parameter
parameters:
slugger.class: App\Utils\Slugger

services:
app.slugger:
class: '%slugger.class%'

This practice is cumbersome and completely unnecessary for your own services.

.. best-practice::

Don't define parameters for the classes of your services.

This practice was wrongly adopted from third-party bundles. When Symfony
introduced its service container, some developers used this technique to easily
allow overriding services. However, overriding a service by just changing its
class name is a very rare use case because, frequently, the new service has
different constructor arguments.

Using a Persistence Layer
-------------------------

Expand Down
72 changes: 42 additions & 30 deletions best_practices/controllers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ Symfony follows the philosophy of *"thin controllers and fat models"*. This
means that controllers should hold just the thin layer of *glue-code*
needed to coordinate the different parts of the application.

As a rule of thumb, you should follow the 5-10-20 rule, where controllers should
only define 5 variables or less, contain 10 actions or less and include 20 lines
of code or less in each action. This isn't an exact science, but it should
help you realize when code should be refactored out of the controller and
into a service.
Your controller methods should just call to other services, trigger some events
if needed and then return a response, but they should not contain any actual
business logic. If they do, refactor it out of the controller and into a service.

.. best-practice::

Make your controller extend the FrameworkBundle base controller and use
annotations to configure routing, caching and security whenever possible.
Make your controller extend the ``AbstractController`` base controller
provided by Symfony and use annotations to configure routing, caching and
security whenever possible.

Coupling the controllers to the underlying framework allows you to leverage
all of its features and increases your productivity.
Expand All @@ -33,6 +32,18 @@ Overall, this means you should aggressively decouple your business logic
from the framework while, at the same time, aggressively coupling your controllers
and routing *to* the framework in order to get the most out of it.

Controller Action Naming
------------------------

.. best-practice::

Don't add the ``Action`` suffix to the methods of the controller actions.

The first Symfony versions required that controller method names ended in
``Action`` (e.g. ``newAction()``, ``showAction()``). This suffix became optional
when annotations were introduced for controllers. In modern Symfony applications
this suffix is neither required nor recommended, so you can safely remove it.

Routing Configuration
---------------------

Expand Down Expand Up @@ -94,32 +105,32 @@ for the homepage of our app:
namespace App\Controller;

use App\Entity\Post;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends Controller
class DefaultController extends AbstractController
{
/**
* @Route("/", name="homepage")
*/
public function indexAction()
public function index()
{
$posts = $this->getDoctrine()
->getRepository(Post::class)
->findLatest();

return $this->render('default/index.html.twig', array(
return $this->render('default/index.html.twig', [
'posts' => $posts,
));
]);
}
}

Fetching Services
-----------------

If you extend the base ``Controller`` class, you can access services directly from
the container via ``$this->container->get()`` or ``$this->get()``. But instead, you
should use dependency injection to fetch services: most easily done by
If you extend the base ``AbstractController`` class, you can't access services
directly from the container via ``$this->container->get()`` or ``$this->get()``.
Instead, you must use dependency injection to fetch services: most easily done by
:ref:`type-hinting action method arguments <controller-accessing-services>`:

.. best-practice::
Expand Down Expand Up @@ -153,40 +164,41 @@ For example:
/**
* @Route("/{id}", name="admin_post_show")
*/
public function showAction(Post $post)
public function show(Post $post)
{
$deleteForm = $this->createDeleteForm($post);

return $this->render('admin/post/show.html.twig', array(
return $this->render('admin/post/show.html.twig', [
'post' => $post,
'delete_form' => $deleteForm->createView(),
));
]);
}

Normally, you'd expect a ``$id`` argument to ``showAction()``. Instead, by
creating a new argument (``$post``) and type-hinting it with the ``Post``
class (which is a Doctrine entity), the ParamConverter automatically queries
for an object whose ``$id`` property matches the ``{id}`` value. It will
also show a 404 page if no ``Post`` can be found.
Normally, you'd expect a ``$id`` argument to ``show()``. Instead, by creating a
new argument (``$post``) and type-hinting it with the ``Post`` class (which is a
Doctrine entity), the ParamConverter automatically queries for an object whose
``$id`` property matches the ``{id}`` value. It will also show a 404 page if no
``Post`` can be found.

When Things Get More Advanced
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The above example works without any configuration because the wildcard name ``{id}`` matches
the name of the property on the entity. If this isn't true, or if you have
even more complex logic, the easiest thing to do is just query for the entity
manually. In our application, we have this situation in ``CommentController``:
The above example works without any configuration because the wildcard name
``{id}`` matches the name of the property on the entity. If this isn't true, or
if you have even more complex logic, the easiest thing to do is just query for
the entity manually. In our application, we have this situation in
``CommentController``:

.. code-block:: php

/**
* @Route("/comment/{postSlug}/new", name = "comment_new")
*/
public function newAction(Request $request, $postSlug)
public function new(Request $request, $postSlug)
{
$post = $this->getDoctrine()
->getRepository(Post::class)
->findOneBy(array('slug' => $postSlug));
->findOneBy(['slug' => $postSlug]);

if (!$post) {
throw $this->createNotFoundException();
Expand All @@ -209,7 +221,7 @@ flexible:
* @Route("/comment/{postSlug}/new", name = "comment_new")
* @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
*/
public function newAction(Request $request, Post $post)
public function new(Request $request, Post $post)
{
// ...
}
Expand Down
Loading