diff --git a/_build/redirection_map b/_build/redirection_map index 9014eee1f2c..2d3f18a6871 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -460,3 +460,15 @@ /testing/doctrine /testing/database /doctrine/lifecycle_callbacks /doctrine/events /doctrine/event_listeners_subscribers /doctrine/events +/best_practices/index /best_practices +/best_practices/introduction /best_practices +/best_practices/creating-the-project /best_practices +/best_practices/configuration /best_practices +/best_practices/business-logic /best_practices +/best_practices/controllers /best_practices +/best_practices/templates /best_practices +/best_practices/forms /best_practices +/best_practices/i18n /best_practices +/best_practices/security /best_practices +/best_practices/web-assets /best_practices +/best_practices/tests /best_practices diff --git a/best_practices.rst b/best_practices.rst new file mode 100644 index 00000000000..692761d4fda --- /dev/null +++ b/best_practices.rst @@ -0,0 +1,443 @@ +The Symfony Framework Best Practices +==================================== + +This article describes the **best practices for developing web applications with +Symfony** that fit the philosophy envisioned by the original Symfony creators. + +If you don't agree with some of these recommendations, they might be a good +**starting point** that you can then **extend and fit to your specific needs**. +You can even ignore them completely and continue using your own best practices +and methodologies. Symfony is flexible enough to adapt to your needs. + +This article assumes that you already have experience developing Symfony +applications. If you don't, read first the rest of the `Symfony documentation`_. + +.. tip:: + + Symfony provides a sample application called `Symfony Demo`_ that follows + all these best practices, so you can experience them in practice. + +Creating the Project +-------------------- + +Use the Symfony Binary to Create Symfony Applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Symfony binary is an executable command created in your machine when you +`download Symfony`_. It provides multiple utilities, including the simplest way +to create new Symfony applications: + +.. code-block:: terminal + + $ symfony new my_project_name + +Under the hood, this Symfony binary command executes the needed `Composer`_ +command to :ref:`create a new Symfony application ` +based on the current stable version. + +Use the Default Directory Structure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unless your project follows a development practice that imposes a certain +directory structure, follow the default Symfony directory structure. It's flat, +self-explanatory and not coupled to Symfony: + +.. code-block:: text + + your_project/ + ├─ assets/ + ├─ bin/ + │ └─ console + ├─ config/ + │ ├─ packages/ + │ └─ services.yaml + └─ public/ + │ ├─ build/ + │ └─ index.php + ├─ src/ + │ ├─ Kernel.php + │ ├─ Command/ + │ ├─ Controller/ + │ ├─ DataFixtures/ + │ ├─ Entity/ + │ ├─ EventSubscriber/ + │ ├─ Form/ + │ ├─ Migrations/ + │ ├─ Repository/ + │ ├─ Security/ + │ └─ Twig/ + ├─ templates/ + ├─ tests/ + ├─ translations/ + ├─ var/ + │ ├─ cache/ + │ └─ log/ + └─ vendor/ + +Configuration +------------- + +Use Environment Variables for Infrastructure Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These are the options that change from one machine to another (e.g. from your +development machine to the production server) but which don't change the +application behavior. + +:ref:`Use env vars in your project ` to define these options +and create multiple ``.env`` files to :ref:`configure env vars per environment `. + +Use Parameters for Application Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These are the options used to modify the application behavior, such as the sender +of email notifications, or the enabled `feature toggles`_. Their value doesn't +change per machine, so don't define them as environment variables. + +Define these options as :ref:`parameters ` in the +``config/services.yaml`` file. You can override these options per +:ref:`environment ` in the ``config/services_dev.yaml`` +and ``config/services_prod.yaml`` files. + +Use Short and Prefixed Parameter Names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Consider using ``app.`` as the prefix of your :ref:`parameters ` +to avoid collisions with Symfony and third-party bundles/libraries parameters. +Then, use just one or two words to describe the purpose of the parameter: + +.. code-block:: yaml + + # config/services.yaml + parameters: + # don't do this: 'dir' is too generic and it doesn't convey any meaning + app.dir: '...' + # do this: short but easy to understand names + app.contents_dir: '...' + # it's OK to use dots, underscores, dashes or nothing, but always + # be consistent and use the same format for all the parameters + app.dir.contents: '...' + app.contents-dir: '...' + +Use Constants to Define Options that Rarely Change +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configuration options like the number of items to display in some listing rarely +change. Instead of defining them as :ref:`service container parameters `, +define them as PHP constants in the related classes. Example:: + + // src/Entity/Post.php + namespace App\Entity; + + class Post + { + public const NUMBER_OF_ITEMS = 10; + + // ... + } + +The main advantage of constants is that you can use them everywhere, including +Twig templates and Doctrine entities, whereas parameters are only available +from places with access to the :doc:`service container `. + +The only notable disadvantage of using constants for this kind of configuration +values is that it's complicated to redefine their values in your tests. + +Business Logic +-------------- + +Don't Create any Bundle to Organize your Application Logic +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When Symfony 2.0 was released, applications used :doc:`bundles ` to +divide their code into logical features: UserBundle, ProductBundle, +InvoiceBundle, etc. However, a bundle is meant to be something that can be +reused as a stand-alone piece of software. + +If you need to reuse some feature in your projects, create a bundle for it (in a +private repository, to not make it publicly available). For the rest of your +application code, use PHP namespaces to organize code instead of bundles. + +Use Autowiring to Automate the Configuration of Application Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:doc:`Service autowiring ` is a feature that +reads the type-hints on your constructor (or other methods) and automatically +passes the correct services to each method, making unnecessary to configure +services explicitly and simplifying the application maintenance. + +Use it in combination with :ref:`service autoconfiguration ` +to also add :doc:`service tags ` to the services +needing them, such as Twig extensions, event subscribers, etc. + +Services Should be Private Whenever Possible +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:ref:`Make services private ` to prevent you from accessing +those services via ``$container->get()``. Instead, you will need to use proper +dependency injection. + +Use the YAML Format to Configure your Own Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you use the :ref:`default services.yaml configuration `, +most services will be configured automatically. However, in some edge cases +you'll need to configure services (or parts of them) manually. + +YAML is the format recommended to configure services because it's friendly to +newcomers and concise, but Symfony also supports XML and PHP configuration. + +Use Annotations to Define the Doctrine Entity Mapping +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Doctrine entities are plain PHP objects that you store in some "database". +Doctrine only knows about your entities through the mapping metadata configured +for your model classes. + +Doctrine supports several metadata formats, but it's recommended to use +annotations because they are by far the most convenient and agile way of setting +up and looking for mapping information. + +Controllers +----------- + +Make your Controller Extend the ``AbstractController`` Base Controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides a :ref:`base controller ` +which includes shortcuts for the most common needs such as rendering templates +or checking security permissions. + +Extending your controllers from this base controller couples your application +to Symfony. Coupling is generally wrong, but it may be OK in this case because +controllers shouldn't contain any business logic. Controllers should contain +nothing more than a few lines of *glue-code*, so you are not coupling the +important parts of your application. + +.. _best-practice-controller-annotations: + +Use Annotations to Configure Routing, Caching and Security +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using annotations for routing, caching and security simplifies configuration. +You don't need to browse several files created with different formats (YAML, XML, +PHP): all the configuration is just where you need it and it only uses one format. + +Don't Use Annotations to Configure the Controller Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``@Template`` annotation is useful, but also involves some *magic*. +Moreover, most of the time ``@Template`` is used without any parameters, which +makes it more difficult to know which template is being rendered. It also hides +the fact that a controller should always return a ``Response`` object. + +Use Dependency Injection to Get Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you extend the base ``AbstractController``, 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 by +:ref:`type-hinting action method arguments ` or +constructor arguments. + +Use ParamConverters If They Are 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. + +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 `). + +Templates +--------- + +Use Snake Case for Template Names and Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use lowercased snake_case for template names, directories and variables (e.g. +``user_profile`` instead of ``userProfile`` and ``product/edit_form.html.twig`` +instead of ``Product/EditForm.html.twig``). + +Prefix Template Fragments with an Underscore +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Template fragments, also called *"partial templates"*, allow to +:ref:`reuse template contents `. Prefix their names +with an underscore to better differentiate them from complete templates (e.g. +``_user_metadata.html.twig`` or ``_caution_message.html.twig``). + +Forms +----- + +Define your Forms as PHP Classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating :ref:`forms in classes ` allows to reuse +them in different parts of the application. Besides, not creating forms in +controllers simplify the code and maintenance of the controllers. + +Add Form Buttons in Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Form classes should be agnostic to where they will be used. For example, the +button of a form used to both create and edit items should change from "Add new" +to "Save changes" depending on where it's used. + +Instead of adding buttons in form classes or the controllers, it's recommended +to add buttons in the templates. This also improves the separation of concerns, +because the button styling (CSS class and other attributes) is defined in the +template instead of in a PHP class. + +Define Validation Constraints on the Underlying Object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Attaching :doc:`validation constraints ` to form fields +instead of to the mapped object prevents the validation from being reused in +other forms or other places where the object is used. + +.. _best-practice-handle-form: + +Use a Single Action to Render and Process the Form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:ref:`Rendering forms ` and :ref:`processing forms ` +are two of the main tasks when handling forms. Both are too similar (most of the +times, almost identical), so it's much simpler to let a single controller action +handle everything. + +Internationalization +-------------------- + +Use the XLIFF Format for Your Translation Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Of all the translation formats supported by Symfony (PHP, Qt, ``.po``, ``.mo``, +JSON, CSV, INI, etc.) XLIFF and gettext have the best support in the tools used +by professional translators. And since it's based on XML, you can validate XLIFF +file contents as you write them. + +Symfony also supports notes in XLIFF files, making them more user-friendly for +translators. At the end, good translations are all about context, and these +XLIFF notes allow you to define that context. + +Use Keys for Translations Instead of Content Strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using keys simplifies the management of the translation files because you can +change the original contents in templates, controllers and services without +having to update all of the translation files. + +Keys should always describe their *purpose* and *not* their location. For +example, if a form has a field with the label "Username", then a nice key +would be ``label.username``, *not* ``edit_form.label.username``. + +Security +-------- + +Define a Single Firewall +~~~~~~~~~~~~~~~~~~~~~~~~ + +Unless you have two legitimately different authentication systems and users +(e.g. form login for the main site and a token system for your API only), it's +recommended to have only one firewall to keep things simple. + +Additionally, you should use the ``anonymous`` key under your firewall. If you +require users to be logged in for different sections of your site, use the +:doc:`access_control ` option. + +Use the ``auto`` Password Hasher +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :ref:`auto password hasher ` automatically +selects the best possible encoder/hasher depending on your PHP installation. +Currently, it tries to use ``sodium`` by default and falls back to ``bcrypt``. + +Use Voters to Implement Fine-grained Security Restrictions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your security logic is complex, you should create custom +:doc:`security voters ` instead of defining long expressions +inside the ``@Security`` annotation. + +Web Assets +---------- + +Use Webpack Encore to Process Web Assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Web assets are things like CSS, JavaScript and image files that make the +frontend of your site look and work great. `Webpack`_ is the leading JavaScript +module bundler that compiles, transforms and packages assets for usage in a browser. + +:doc:`Webpack Encore ` is a JavaScript library that gets rid of most +of Webpack complexity without hiding any of its features or distorting its usage +and philosophy. It was originally created for Symfony applications, but it works +for any application using any technology. + +Tests +----- + +Smoke Test your URLs +~~~~~~~~~~~~~~~~~~~~ + +In software engineering, `smoke testing`_ consists of *"preliminary testing to +reveal simple failures severe enough to reject a prospective software release"*. +Using :ref:`PHPUnit data providers ` you can define a +functional test that checks that all application URLs load successfully:: + + // tests/ApplicationAvailabilityFunctionalTest.php + namespace App\Tests; + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + + class ApplicationAvailabilityFunctionalTest extends WebTestCase + { + /** + * @dataProvider urlProvider + */ + public function testPageIsSuccessful($url) + { + $client = self::createClient(); + $client->request('GET', $url); + + $this->assertResponseIsSuccessful(); + } + + public function urlProvider() + { + yield ['/']; + yield ['/posts']; + yield ['/post/fixture-post-1']; + yield ['/blog/category/fixture-category']; + yield ['/archives']; + // ... + } + } + +Add this test while creating your application because it requires little effort +and checks that none of your pages returns an error. Later you'll add more +specific tests for each page. + +Hardcode URLs in a Functional Test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In Symfony applications it's recommended to :ref:`generate URLs ` +using routes to automatically update all links when a URL changes. However, if a +public URL changes, users won't be able to browse it unless you set up a +redirection to the new URL. + +That's why it's recommended to use raw URLs in tests instead of generating them +from routes. Whenever a route changes, tests will break and you'll know that +you must set up a redirection. + +.. _`Symfony documentation`: https://symfony.com/doc +.. _`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/best_practices/business-logic.rst b/best_practices/business-logic.rst deleted file mode 100644 index a6f07c73ec6..00000000000 --- a/best_practices/business-logic.rst +++ /dev/null @@ -1,282 +0,0 @@ -Organizing Your Business Logic -============================== - -In computer software, **business logic** or domain logic is "the part of the -program that encodes the real-world business rules that determine how data can -be created, displayed, stored, and changed" (read `full definition`_). - -In Symfony applications, business logic is all the custom code you write for -your app that's not specific to the framework (e.g. routing and controllers). -Domain classes, Doctrine entities and regular PHP classes that are used as -services are good examples of business logic. - -For most projects, you should store all your code inside the ``src/`` directory. -Inside here, you can create whatever directories you want to organize things: - -.. code-block:: text - - symfony-project/ - ├─ config/ - ├─ public/ - ├─ src/ - │ └─ Utils/ - │ └─ MyClass.php - ├─ tests/ - ├─ var/ - └─ vendor/ - -.. _services-naming-and-format: - -Services: Naming and Configuration ----------------------------------- - -.. best-practice:: - - Use autowiring to automate the configuration of application services. - -:doc:`Service 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 ` to the services needing 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/``:: - - // src/Utils/Slugger.php - namespace App\Utils; - - class Slugger - { - public function slugify(string $value): string - { - // ... - } - } - -If you're using the :ref:`default services.yaml configuration `, -this class is auto-registered as a service with the ID ``App\Utils\Slugger`` (to -prevent against typos, import the class and write ``Slugger::class`` 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 other service or controller class, -such as the ``AdminController``:: - - use App\Utils\Slugger; - - public function create(Request $request, Slugger $slugger) - { - // ... - - if ($form->isSubmitted() && $form->isValid()) { - $slug = $slugger->slugify($post->getTitle()); - $post->setSlug($slug); - - // ... - } - } - -Services can also be :ref:`public or private `. If you use the -:ref:`default services.yaml 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 --------------------- - -If you use the :ref:`default services.yaml configuration `, -most services will be configured automatically. However, in some edge cases -you'll need to configure services (or parts of them) manually. - -.. best-practice:: - - Use the YAML format to configure your own services. - -This is controversial, and in our experience, YAML and XML usage is evenly -distributed among developers, with a slight preference towards YAML. -Both formats have the same performance, so this is ultimately a matter of -personal taste. - -We recommend YAML because it's friendly to newcomers and concise, but you can -use whatever format you like. - -Using a Persistence Layer -------------------------- - -Symfony is an HTTP framework that only cares about generating an HTTP response -for each HTTP request. That's why Symfony doesn't provide a way to talk to -a persistence layer (e.g. database, external API). You can choose whatever -library or strategy you want for this. - -In practice, many Symfony applications rely on the independent -`Doctrine project`_ to define their model using entities and repositories. - -:doc:`Doctrine ` support is not enabled by default in Symfony, so you -must install it first by adding the ``orm`` :ref:`Symfony pack ` -to your application: - -.. code-block:: terminal - - $ composer require symfony/orm-pack - -Just like with business logic, we recommend storing Doctrine entities in the -``src/Entity/`` directory. - -The three entities defined by our sample blog application are a good example: - -.. code-block:: text - - symfony-project/ - ├─ ... - └─ src/ - └─ Entity/ - ├─ Comment.php - ├─ Post.php - └─ User.php - -Doctrine Mapping Information -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Doctrine entities are plain PHP objects that you store in some "database". -Doctrine only knows about your entities through the mapping metadata configured -for your model classes. Doctrine supports four metadata formats: YAML, XML, -PHP and annotations. - -.. best-practice:: - - Use annotations to define the mapping information of the Doctrine entities. - -Annotations are by far the most convenient and agile way of setting up and -looking for mapping information:: - - namespace App\Entity; - - use Doctrine\Common\Collections\ArrayCollection; - use Doctrine\ORM\Mapping as ORM; - - /** - * @ORM\Entity - */ - class Post - { - const NUMBER_OF_ITEMS = 10; - - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - private $id; - - /** - * @ORM\Column(type="string") - */ - private $title; - - /** - * @ORM\Column(type="string") - */ - private $slug; - - /** - * @ORM\Column(type="text") - */ - private $content; - - /** - * @ORM\Column(type="string") - */ - private $authorEmail; - - /** - * @ORM\Column(type="datetime") - */ - private $publishedAt; - - /** - * @ORM\OneToMany( - * targetEntity="App\Entity\Comment", - * mappedBy="post", - * orphanRemoval=true - * ) - * @ORM\OrderBy({"publishedAt"="ASC"}) - */ - private $comments; - - public function __construct() - { - $this->publishedAt = new \DateTime(); - $this->comments = new ArrayCollection(); - } - - // getters and setters ... - } - -All formats have the same performance, so this is once again ultimately a -matter of taste. - -Data Fixtures -~~~~~~~~~~~~~ - -As fixtures support is not enabled by default in Symfony, you should execute -the following command to install the Doctrine fixtures bundle: - -.. code-block:: terminal - - $ composer require --dev doctrine/doctrine-fixtures-bundle - -Then, this bundle is enabled automatically, but only for the ``dev`` and -``test`` environments:: - - // config/bundles.php - return [ - // ... - Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], - ]; - -We recommend creating just *one* `fixture class`_ for simplicity, though -you're welcome to have more if that class gets quite large. - -Assuming you have at least one fixtures class and that the database access -is configured properly, you can load your fixtures by executing the following -command: - -.. code-block:: terminal - - $ php bin/console doctrine:fixtures:load - - Careful, database will be purged. Do you want to continue Y/N ? Y - > purging database - > loading App\DataFixtures\ORM\LoadFixtures - -Coding Standards ----------------- - -The Symfony source code follows the `PSR-1`_ and `PSR-2`_ coding standards that -were defined by the PHP community. You can learn more about -:doc:`the Symfony Coding standards ` and even -use the `PHP-CS-Fixer`_, which is a command-line utility that can fix the -coding standards of an entire codebase in a matter of seconds. - ----- - -Next: :doc:`/best_practices/controllers` - -.. _`full definition`: https://en.wikipedia.org/wiki/Business_logic -.. _`Doctrine project`: http://www.doctrine-project.org/ -.. _`fixture class`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html#writing-simple-fixtures -.. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ -.. _`PSR-2`: https://www.php-fig.org/psr/psr-2/ -.. _`PHP-CS-Fixer`: https://github.com/FriendsOfPHP/PHP-CS-Fixer diff --git a/best_practices/configuration.rst b/best_practices/configuration.rst deleted file mode 100644 index 80d4273c6a1..00000000000 --- a/best_practices/configuration.rst +++ /dev/null @@ -1,189 +0,0 @@ -Configuration -============= - -Configuration usually involves different application parts (such as infrastructure -and security credentials) and different environments (development, production). -That's why Symfony recommends that you split the application configuration into -three parts. - -Infrastructure-Related Configuration ------------------------------------- - -These are the options that change from one machine to another (e.g. from your -development machine to the production server) but which don't change the -application behavior. - -.. best-practice:: - - Define the infrastructure-related configuration options as - :ref:`environment variables `. During development, use the - ``.env`` and ``.env.local`` files at the root of your project to set these. - -By default, Symfony adds these types of options to the ``.env`` file when -installing new dependencies in the app: - -.. code-block:: bash - - # .env - ###> doctrine/doctrine-bundle ### - DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/blog.sqlite - ###< doctrine/doctrine-bundle ### - - ###> symfony/swiftmailer-bundle ### - MAILER_URL=smtp://localhost?encryption=ssl&auth_mode=login&username=&password= - ###< symfony/swiftmailer-bundle ### - - # ... - -These options aren't defined inside the ``config/services.yaml`` file because -they have nothing to do with the application's behavior. In other words, your -application doesn't care about the location of your database or the credentials -to access to it, as long as the database is correctly configured. - -To override these variables with machine-specific or sensitive values, create a -``.env.local`` file. This file should not be added to version control. - -.. caution:: - - Beware that dumping the contents of the ``$_SERVER`` and ``$_ENV`` variables - or outputting the ``phpinfo()`` contents will display the values of the - environment variables, exposing sensitive information such as the database - credentials. - -Canonical Parameters -~~~~~~~~~~~~~~~~~~~~ - -.. best-practice:: - - Define all your application's env vars in the ``.env`` file. - -Symfony includes a configuration file called ``.env`` at the project root, which -stores the canonical list of environment variables for the application. This -file should be stored in version control and so should only contain non-sensitive -default values. - -.. caution:: - - Applications created before November 2018 had a slightly different system, - involving a ``.env.dist`` file. For information about upgrading, see: - :doc:`/configuration/dot-env-changes`. - -Application-Related Configuration ---------------------------------- - -.. best-practice:: - - Define the application behavior related configuration options in the - ``config/services.yaml`` file. - -The ``services.yaml`` file contains the options used by the application to -modify its behavior, such as the sender of email notifications, or the enabled -`feature toggles`_. Defining these values in ``.env`` file would add an extra -layer of configuration that's not needed because you don't need or want these -configuration values to change on each server. - -The configuration options defined in the ``services.yaml`` may vary from one -:ref:`environment ` to another. That's why Symfony -supports defining ``config/services_dev.yaml`` and ``config/services_prod.yaml`` -files so that you can override specific values for each environment. - -Constants vs Configuration Options -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -One of the most common errors when defining application configuration is to -create new options for values that never change, such as the number of items for -paginated results. - -.. best-practice:: - - Use constants to define configuration options that rarely change. - -The traditional approach for defining configuration options has caused many -Symfony applications to include an option like the following, which would be -used to control the number of posts to display on the blog homepage: - -.. code-block:: yaml - - # config/services.yaml - parameters: - homepage.number_of_items: 10 - -If you've done something like this in the past, it's likely that you've in fact -*never* actually needed to change that value. Creating a configuration -option for a value that you are never going to configure just isn't necessary. -Our recommendation is to define these values as constants in your application. -You could, for example, define a ``NUMBER_OF_ITEMS`` constant in the ``Post`` entity:: - - // src/Entity/Post.php - namespace App\Entity; - - class Post - { - const NUMBER_OF_ITEMS = 10; - - // ... - } - -The main advantage of defining constants is that you can use their values -everywhere in your application. When using parameters, they are only available -from places with access to the Symfony container. - -Constants can be used for example in your Twig templates thanks to the -`constant() function`_: - -.. code-block:: html+twig - -

- Displaying the {{ constant('NUMBER_OF_ITEMS', post) }} most recent results. -

- -And Doctrine entities and repositories can access these values too, whereas they -cannot access the container parameters:: - - namespace App\Repository; - - use App\Entity\Post; - use Doctrine\ORM\EntityRepository; - - class PostRepository extends EntityRepository - { - public function findLatest($limit = Post::NUMBER_OF_ITEMS) - { - // ... - } - } - -The only notable disadvantage of using constants for this kind of configuration -values is that it's complicated to redefine their values in your tests. - -Parameter Naming ----------------- - -.. best-practice:: - - The name of your configuration parameters should be as short as possible and - should include a common prefix for the entire application. - -Using ``app.`` as the prefix of your parameters is a common practice to avoid -collisions with Symfony and third-party bundles/libraries parameters. Then, use -just one or two words to describe the purpose of the parameter: - -.. code-block:: yaml - - # config/services.yaml - parameters: - # don't do this: 'dir' is too generic and it doesn't convey any meaning - app.dir: '...' - # do this: short but easy to understand names - app.contents_dir: '...' - # it's OK to use dots, underscores, dashes or nothing, but always - # be consistent and use the same format for all the parameters - app.dir.contents: '...' - app.contents-dir: '...' - ----- - -Next: :doc:`/best_practices/business-logic` - -.. _`feature toggles`: https://en.wikipedia.org/wiki/Feature_toggle -.. _`constant() function`: https://twig.symfony.com/doc/2.x/functions/constant.html diff --git a/best_practices/controllers.rst b/best_practices/controllers.rst deleted file mode 100644 index 83e38ed0391..00000000000 --- a/best_practices/controllers.rst +++ /dev/null @@ -1,237 +0,0 @@ -Controllers -=========== - -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. - -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 ``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. - -And since your controllers should be thin and contain nothing more than a -few lines of *glue-code*, spending hours trying to decouple them from your -framework doesn't benefit you in the long run. The amount of time *wasted* -isn't worth the benefit. - -In addition, using annotations for routing, caching and security simplifies -configuration. You don't need to browse tens of files created with different -formats (YAML, XML, PHP): all the configuration is just where you need it -and it only uses one format. - -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 ---------------------- - -To load routes defined as annotations in your controllers, run -``composer require doctrine/annotations``. Thanks to the :ref:`Flex recipe `, -a ``config/routes/annotations.yaml`` file will be created: - -.. code-block:: yaml - - # config/routes/annotations.yaml - controllers: - resource: '../../src/Controller/' - type: annotation - -This configuration will load annotations from any controller stored inside the -``src/Controller/`` directory and even from its subdirectories. So if your application -defines lots of controllers, it's perfectly ok to reorganize them into subdirectories: - -.. code-block:: text - - / - ├─ ... - └─ src/ - ├─ ... - └─ Controller/ - ├─ DefaultController.php - ├─ ... - ├─ Api/ - │ ├─ ... - │ └─ ... - └─ Backend/ - ├─ ... - └─ ... - -Template Configuration ----------------------- - -.. best-practice:: - - Don't use the ``@Template`` annotation to configure the template used by - the controller. - -The ``@Template`` annotation is useful, but also involves some magic. We -don't think its benefit is worth the magic, and so recommend against using -it. - -Most of the time, ``@Template`` is used without any parameters, which makes it -more difficult to know which template is being rendered. It also hides the fact -that a controller should always return a Response object (unless you're using a -view layer). - -What does the Controller look like ----------------------------------- - -Considering all this, here is an example of what the controller should look like -for the homepage of our app:: - - namespace App\Controller; - - use App\Entity\Post; - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController extends AbstractController - { - /** - * @Route("/", name="homepage") - */ - public function index() - { - $posts = $this->getDoctrine() - ->getRepository(Post::class) - ->findLatest(); - - return $this->render('default/index.html.twig', [ - 'posts' => $posts, - ]); - } - } - -Fetching Services ------------------ - -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 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. - -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 ------------------------- - -If you're using Doctrine, then you can *optionally* use the `ParamConverter`_ -to automatically query for an entity and pass it as an argument to your controller. - -.. best-practice:: - - Use the ParamConverter trick to automatically query for Doctrine entities - when it's simple and convenient. - -For example:: - - use App\Entity\Post; - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route("/{id}", name="admin_post_show") - */ - public function show(Post $post) - { - $deleteForm = $this->createDeleteForm($post); - - return $this->render('admin/post/show.html.twig', [ - 'post' => $post, - 'delete_form' => $deleteForm->createView(), - ]); - } - -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, your best choice is to query for -the entity manually. In our application, we have this situation in -``CommentController``:: - - /** - * @Route("/comment/{postSlug}/new", name="comment_new") - */ - public function new(Request $request, $postSlug) - { - $post = $this->getDoctrine() - ->getRepository(Post::class) - ->findOneBy(['slug' => $postSlug]); - - if (!$post) { - throw $this->createNotFoundException(); - } - - // ... - } - -You can also use the ``@ParamConverter`` configuration, which is infinitely -flexible:: - - use App\Entity\Post; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route("/comment/{postSlug}/new", name="comment_new") - * @ParamConverter("post", options={"mapping"={"postSlug"="slug"}}) - */ - public function new(Request $request, Post $post) - { - // ... - } - -The point is this: the ParamConverter shortcut is great for most situations. -However, there is nothing wrong with querying for entities directly if the -ParamConverter would get complicated. - -Pre and Post Hooks ------------------- - -If you need to execute some code before or after the execution of your controllers, -you can use the EventDispatcher component to -:doc:`set up before and after filters `. - ----- - -Next: :doc:`/best_practices/templates` - -.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html diff --git a/best_practices/creating-the-project.rst b/best_practices/creating-the-project.rst deleted file mode 100644 index 2c5ea8a5d55..00000000000 --- a/best_practices/creating-the-project.rst +++ /dev/null @@ -1,100 +0,0 @@ -Creating the Project -==================== - -Installing Symfony ------------------- - -.. best-practice:: - - Use Composer and Symfony Flex to create and manage Symfony applications. - -`Composer`_ is the package manager used by modern PHP applications to manage -their dependencies. `Symfony Flex`_ is a Composer plugin designed to automate -some of the most common tasks performed in Symfony applications. Using Flex is -optional but recommended because it improves your productivity significantly. - -.. best-practice:: - - Use the Symfony Skeleton to create new Symfony-based projects. - -The `Symfony Skeleton`_ is a minimal and empty Symfony project which you can -base your new projects on. Unlike past Symfony versions, this skeleton installs -the absolute bare minimum amount of dependencies to make a fully working Symfony -project. Read the :doc:`/setup` article to learn more about installing Symfony. - -.. _linux-and-mac-os-x-systems: -.. _windows-systems: - -Creating the Blog Application ------------------------------ - -In your command console, browse to a directory where you have permission to -create files and execute the following commands: - -.. code-block:: terminal - - $ cd projects/ - $ composer create-project symfony/skeleton blog - -This command creates a new directory called ``blog`` that contains a fresh new -project based on the most recent stable Symfony version available. - -.. seealso:: - - Check out the :ref:`technical requirements for running Symfony applications `. - -Structuring the Application ---------------------------- - -After creating the application, enter the ``blog/`` directory and you'll see a -number of files and directories generated automatically. These are the most -important ones: - -.. code-block:: text - - blog/ - ├─ bin/ - │ └─ console - ├─ config/ - └─ public/ - │ └─ index.php - ├─ src/ - │ └─ Kernel.php - ├─ var/ - │ ├─ cache/ - │ └─ log/ - └─ vendor/ - -This file and directory hierarchy is the convention proposed by Symfony to -structure your applications. It's recommended to keep this structure because it's -easy to navigate and most directory names are self-explanatory, but you can -:doc:`override the location of any Symfony directory `: - -Application Bundles -~~~~~~~~~~~~~~~~~~~ - -When Symfony 2.0 was released, most developers naturally adopted the symfony -1.x way of dividing applications into logical modules. That's why many Symfony -applications used bundles to divide their code into logical features: UserBundle, -ProductBundle, InvoiceBundle, etc. - -But a bundle is *meant* to be something that can be reused as a stand-alone -piece of software. If UserBundle cannot be used *"as is"* in other Symfony -applications, then it shouldn't be its own bundle. Moreover, if InvoiceBundle -depends on ProductBundle, then there's no advantage to having two separate bundles. - -.. best-practice:: - - Don't create any bundle to organize your application logic. - -Symfony applications can still use third-party bundles (installed in ``vendor/``) -to add features, but you should use PHP namespaces instead of bundles to organize -your own code. - ----- - -Next: :doc:`/best_practices/configuration` - -.. _`Composer`: https://getcomposer.org/ -.. _`Symfony Flex`: https://github.com/symfony/flex -.. _`Symfony Skeleton`: https://github.com/symfony/skeleton diff --git a/best_practices/forms.rst b/best_practices/forms.rst deleted file mode 100644 index 07596e73e54..00000000000 --- a/best_practices/forms.rst +++ /dev/null @@ -1,223 +0,0 @@ -Forms -===== - -Forms are one of the most misused Symfony components due to its vast scope and -endless list of features. In this chapter we'll show you some of the best -practices so you can leverage forms but get work done quickly. - -Building Forms --------------- - -.. best-practice:: - - Define your forms as PHP classes. - -The Form component allows you to build forms right inside your controller code. -This is perfectly fine if you don't need to reuse the form somewhere else. But -for organization and reuse, we recommend that you define each form in its own -PHP class:: - - namespace App\Form; - - use App\Entity\Post; - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\Extension\Core\Type\DateTimeType; - use Symfony\Component\Form\Extension\Core\Type\EmailType; - use Symfony\Component\Form\Extension\Core\Type\TextareaType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - - class PostType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('title') - ->add('summary', TextareaType::class) - ->add('content', TextareaType::class) - ->add('authorEmail', EmailType::class) - ->add('publishedAt', DateTimeType::class) - ; - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'data_class' => Post::class, - ]); - } - } - -.. best-practice:: - - Put the form type classes in the ``App\Form`` namespace, unless you - use other custom form classes like data transformers. - -To use the class, use ``createForm()`` and pass the fully qualified class name:: - - // ... - use App\Form\PostType; - - // ... - public function new(Request $request) - { - $post = new Post(); - $form = $this->createForm(PostType::class, $post); - - // ... - } - -Form Button Configuration -------------------------- - -Form classes should try to be agnostic to *where* they will be used. This -makes them easier to re-use later. - -.. best-practice:: - - Add buttons in the templates, not in the form classes or the controllers. - -The Symfony Form component allows you to add buttons as fields on your form. -This is a nice way to simplify the template that renders your form. But if you -add the buttons directly in your form class, this would effectively limit the -scope of that form:: - - class PostType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - // ... - ->add('save', SubmitType::class, ['label' => 'Create Post']) - ; - } - - // ... - } - -This form *may* have been designed for creating posts, but if you wanted -to reuse it for editing posts, the button label would be wrong. Instead, -some developers configure form buttons in the controller:: - - namespace App\Controller\Admin; - - use App\Entity\Post; - use App\Form\PostType; - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Form\Extension\Core\Type\SubmitType; - use Symfony\Component\HttpFoundation\Request; - - class PostController extends AbstractController - { - // ... - - public function new(Request $request) - { - $post = new Post(); - $form = $this->createForm(PostType::class, $post); - $form->add('submit', SubmitType::class, [ - 'label' => 'Create', - 'attr' => ['class' => 'btn btn-default pull-right'], - ]); - - // ... - } - } - -This is also an important error, because you are mixing presentation markup -(labels, CSS classes, etc.) with pure PHP code. Separation of concerns is -always a good practice to follow, so put all the view-related things in the -view layer: - -.. code-block:: html+twig - - {{ form_start(form) }} - {{ form_widget(form) }} - - - {{ form_end(form) }} - -Validation ----------- - -The :ref:`constraints ` option allows you to -attach :doc:`validation constraints ` to any form field. -However, doing that prevents the validation from being reused in other forms or -other places where the mapped object is used. - -.. best-practice:: - - Do not define your validation constraints in the form but on the object the - form is mapped to. - -For example, to validate that the title of the post edited with a form is not -blank, add the following in the ``Post`` object:: - - // src/Entity/Post.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Post - { - /** - * @Assert\NotBlank - */ - public $title; - } - -Rendering the Form ------------------- - -There are a lot of ways to render your form, ranging from rendering the entire -thing in one line to rendering each part of each field independently. The -best way depends on how much customization you need. - -One of the simplest ways - which is especially useful during development - -is to render the form tags and use the ``form_widget()`` function to render -all of the fields: - -.. code-block:: twig - - {{ form_start(form, {attr: {class: 'my-form-class'} }) }} - {{ form_widget(form) }} - {{ form_end(form) }} - -If you need more control over how your fields are rendered, then you should -remove the ``form_widget(form)`` function and render your fields individually. -See :doc:`/form/form_customization` for more information on this and how you -can control *how* the form renders at a global level using form theming. - -Handling Form Submits ---------------------- - -Handling a form submit usually follows a similar template:: - - public function new(Request $request) - { - // build the form ... - - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $entityManager = $this->getDoctrine()->getManager(); - $entityManager->persist($post); - $entityManager->flush(); - - return $this->redirectToRoute('admin_post_show', [ - 'id' => $post->getId() - ]); - } - - // render the template - } - -.. _best-practice-handle-form: - -We recommend that you use a single action for both rendering the form and -handling the form submit. For example, you *could* have a ``new()`` action that -*only* renders the form and a ``create()`` action that *only* processes the form -submit. Both those actions will be almost identical. So it's much simpler to let -``new()`` handle everything. - -Next: :doc:`/best_practices/i18n` diff --git a/best_practices/i18n.rst b/best_practices/i18n.rst deleted file mode 100644 index ca4dd0c4c4b..00000000000 --- a/best_practices/i18n.rst +++ /dev/null @@ -1,82 +0,0 @@ -Internationalization -==================== - -Internationalization and localization adapt the applications and their contents -to the specific region or language of the users. In Symfony this is an opt-in -feature that needs to be installed before using it (``composer require symfony/translation``). - -Translation Source File Location --------------------------------- - -.. best-practice:: - - Store the translation files in the ``translations/`` directory at the root - of your project. - -Your translators' lives will be much easier if all the application translations -are in one central location. - -Translation Source File Format ------------------------------- - -The Symfony Translation component supports lots of different translation -formats: PHP, Qt, ``.po``, ``.mo``, JSON, CSV, INI, etc. - -.. best-practice:: - - Use the XLIFF format for your translation files. - -Of all the available translation formats, only XLIFF and gettext have broad -support in the tools used by professional translators. And since it's based -on XML, you can validate XLIFF file contents as you write them. - -Symfony supports notes in XLIFF files, making them more user-friendly for -translators. At the end, good translations are all about context, and these -XLIFF notes allow you to define that context. - -.. tip:: - - The `PHP Translation Bundle`_ includes advanced extractors that can read - your project and automatically update the XLIFF files. - -Translation Keys ----------------- - -.. best-practice:: - - Always use keys for translations instead of content strings. - -Using keys simplifies the management of the translation files because you can -change the original contents without having to update all of the translation -files. - -Keys should always describe their *purpose* and *not* their location. For -example, if a form has a field with the label "Username", then a nice key -would be ``label.username``, *not* ``edit_form.label.username``. - -Example Translation File ------------------------- - -Applying all the previous best practices, the sample translation file for -English in the application would be: - -.. code-block:: xml - - - - - - - - title.post_list - Post List - - - - - ----- - -Next: :doc:`/best_practices/security` - -.. _`PHP Translation Bundle`: https://github.com/php-translation/symfony-bundle diff --git a/best_practices/index.rst b/best_practices/index.rst deleted file mode 100644 index 8df4abb1364..00000000000 --- a/best_practices/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -Official Symfony Best Practices -=============================== - -.. toctree:: - :hidden: - - introduction - creating-the-project - configuration - business-logic - controllers - templates - forms - i18n - security - web-assets - tests - -.. include:: /best_practices/map.rst.inc diff --git a/best_practices/introduction.rst b/best_practices/introduction.rst deleted file mode 100644 index 7575eb8c408..00000000000 --- a/best_practices/introduction.rst +++ /dev/null @@ -1,106 +0,0 @@ -.. index:: - single: Symfony Framework Best Practices - -The Symfony Framework Best Practices -==================================== - -The Symfony Framework is well-known for being *really* flexible and is used -to build micro-sites, enterprise applications that handle billions of connections -and even as the basis for *other* frameworks. Since its release in July 2011, -the community has learned a lot about what's possible and how to do things *best*. - -These community resources - like blog posts or presentations - have created -an unofficial set of recommendations for developing Symfony applications. -Unfortunately, a lot of these recommendations are unneeded for web applications. -Much of the time, they unnecessarily overcomplicate things and don't follow the -original pragmatic philosophy of Symfony. - -What is this Guide About? -------------------------- - -This guide aims to fix that by describing the **best practices for developing -web applications with the Symfony full-stack Framework**. These are best practices -that fit the philosophy of the framework as envisioned by its original creator -`Fabien Potencier`_. - -.. note:: - - **Best practice** is a noun that means *"a well-defined procedure that is - known to produce near-optimum results"*. And that's exactly what this - guide aims to provide. Even if you don't agree with every recommendation, - we believe these will help you build great applications with less complexity. - -This guide is **specially suited** for: - -* Websites and web applications developed with the full-stack Symfony Framework. - -For other situations, this guide might be a good **starting point** that you can -then **extend and fit to your specific needs**: - -* Bundles shared publicly to the Symfony community; -* Advanced developers or teams who have created their own standards; -* Some complex applications that have highly customized requirements; -* Bundles that may be shared internally within a company. - -We know that old habits die hard and some of you will be shocked by some -of these best practices. But by following these, you'll be able to develop -applications faster, with less complexity and with the same or even higher -quality. It's also a moving target that will continue to improve. - -Keep in mind that these are **optional recommendations** that you and your -team may or may not follow to develop Symfony applications. If you want to -continue using your own best practices and methodologies, you can still do -that. Symfony is flexible enough to adapt to your needs. That will never -change. - -Who this Book Is for (Hint: It's not a Tutorial) ------------------------------------------------- - -Any Symfony developer, whether you are an expert or a newcomer, can read this -guide. But since this isn't a tutorial, you'll need some basic knowledge of -Symfony to follow everything. If you are totally new to Symfony, welcome! and -read the :doc:`Getting Started guides ` first. - -We've deliberately kept this guide short. We won't repeat explanations that -you can find in the vast Symfony documentation, like discussions about Dependency -Injection or front controllers. We'll solely focus on explaining how to do -what you already know. - -The Application ---------------- - -In addition to this guide, a sample application called `Symfony Demo`_ has been -developed with all these best practices in mind. Execute this command to download -the demo application: - -.. code-block:: terminal - - $ composer create-project symfony/symfony-demo - -**The demo application is a simple blog engine**, because that will allow us to -focus on the Symfony concepts and features without getting buried in difficult -implementation details. Instead of developing the application step by step in -this guide, you'll find selected snippets of code through the chapters. - -Don't Update Your Existing Applications ---------------------------------------- - -After reading this handbook, some of you may be considering refactoring your -existing Symfony applications. Our recommendation is sound and clear: you may -use these best practices for **new applications** but **you should not refactor -your existing applications to comply with these best practices**. The reasons -for not doing it are various: - -* Your existing applications are not wrong, they just follow another set of - guidelines; -* A full codebase refactorization is prone to introduce errors in your - applications; -* The amount of work spent on this could be better dedicated to improving - your tests or adding features that provide real value to the end users. - ----- - -Next: :doc:`/best_practices/creating-the-project` - -.. _`Fabien Potencier`: https://connect.symfony.com/profile/fabpot -.. _`Symfony Demo`: https://github.com/symfony/demo diff --git a/best_practices/map.rst.inc b/best_practices/map.rst.inc deleted file mode 100644 index f9dfd0c3e9d..00000000000 --- a/best_practices/map.rst.inc +++ /dev/null @@ -1,11 +0,0 @@ -* :doc:`/best_practices/introduction` -* :doc:`/best_practices/creating-the-project` -* :doc:`/best_practices/configuration` -* :doc:`/best_practices/business-logic` -* :doc:`/best_practices/controllers` -* :doc:`/best_practices/templates` -* :doc:`/best_practices/forms` -* :doc:`/best_practices/i18n` -* :doc:`/best_practices/security` -* :doc:`/best_practices/web-assets` -* :doc:`/best_practices/tests` diff --git a/best_practices/security.rst b/best_practices/security.rst deleted file mode 100644 index f0398919750..00000000000 --- a/best_practices/security.rst +++ /dev/null @@ -1,372 +0,0 @@ -Security -======== - -Authentication and Firewalls (i.e. Getting the User's Credentials) ------------------------------------------------------------------- - -You can configure Symfony to authenticate your users using any method you -want and to load user information from any source. This is a complex topic, but -the :doc:`Security guide ` has a lot of information about this. - -Regardless of your needs, authentication is configured in ``security.yaml``, -primarily under the ``firewalls`` key. - -.. best-practice:: - - Unless you have two legitimately different authentication systems and - users (e.g. form login for the main site and a token system for your - API only), we recommend having only *one* firewall entry with the ``anonymous`` - key enabled. - -Most applications only have one authentication system and one set of users. For -this reason, you only need *one* firewall entry. If you have separated web and -API sections on your site, you will need more firewall entries. But the point is -to keep things simple. - -Additionally, you should use the ``anonymous`` key under your firewall. If -you need to require users to be logged in for different sections of your -site (or maybe nearly *all* sections), use the ``access_control`` area. - -.. best-practice:: - - Use the ``auto`` encoder for hashing your users' passwords. - -.. note:: - - :ref:`Sodium ` is the hashing algorithm as - recommended by industry standards, but this won't be available to you unless - you are using PHP 7.2+ or have the `libsodium`_ extension installed. - -With this in mind, here is the authentication setup from our application, -which uses a login form to load users from the database: - -.. code-block:: yaml - - # config/packages/security.yaml - security: - encoders: - App\Entity\User: auto - - providers: - database_users: - entity: { class: App\Entity\User, property: username } - - firewalls: - secured_area: - pattern: ^/ - anonymous: true - form_login: - check_path: login - login_path: login - - logout: - path: security_logout - target: homepage - - # ... access_control exists, but is not shown here - -.. tip:: - - The source code for our project contains comments that explain each part. - -Authorization (i.e. Denying Access) ------------------------------------ - -Symfony gives you several ways to enforce authorization, including the ``access_control`` -configuration in :doc:`security.yaml `, the -:ref:`@Security annotation ` and using -:ref:`isGranted ` on the ``security.authorization_checker`` -service directly. - -.. best-practice:: - - * For protecting broad URL patterns, use ``access_control``; - * Whenever possible, use the ``@Security`` annotation; - * Check security directly on the ``security.authorization_checker`` service - whenever you have a more complex situation. - -There are also different ways to centralize your authorization logic, like -with a custom security voter: - -.. best-practice:: - - Define a custom security voter to implement fine-grained restrictions. - -.. _best-practices-security-annotation: - -The @Security Annotation ------------------------- - -For controlling access on a controller-by-controller basis, use the ``@Security`` -annotation whenever possible. Placing it above each action makes it consistent and readable. - -In our application, you need the ``ROLE_ADMIN`` in order to create a new post. -Using ``@Security``, this looks like:: - - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; - use Symfony\Component\Routing\Annotation\Route; - // ... - - /** - * Displays a form to create a new Post entity. - * - * @Route("/new", name="admin_post_new") - * @Security("is_granted('ROLE_ADMIN')") - */ - public function new() - { - // ... - } - -Using Expressions for Complex Security Restrictions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If your security logic is a little bit more complex, you can use an :doc:`expression ` -inside ``@Security``. In the following example, a user can only access the -controller if their email matches the value returned by the ``getAuthorEmail()`` -method on the ``Post`` object:: - - use App\Entity\Post; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route("/{id}/edit", name="admin_post_edit") - * @Security("user.getEmail() == post.getAuthorEmail()") - */ - public function edit(Post $post) - { - // ... - } - -Notice that this requires the use of the `ParamConverter`_, which automatically -queries for the ``Post`` object and puts it on the ``$post`` argument. This -is what makes it possible to use the ``post`` variable in the expression. - -This has one major drawback: an expression in an annotation cannot -be reused in other parts of the application. Imagine that you want to add -a link in a template that will only be seen by authors. Right now you'll -need to repeat the expression code using Twig syntax: - -.. code-block:: html+twig - - {% if app.user and app.user.email == post.authorEmail %} - ... - {% endif %} - -A good solution - if your logic is simple enough - can be to add a new method -to the ``Post`` entity that checks if a given user is its author:: - - // src/Entity/Post.php - // ... - - class Post - { - // ... - - /** - * Is the given User the author of this Post? - * - * @return bool - */ - public function isAuthor(User $user = null) - { - return $user && $user->getEmail() === $this->getAuthorEmail(); - } - } - -Now you can reuse this method both in the template and in the security expression:: - - use App\Entity\Post; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; - use Symfony\Component\Routing\Annotation\Route; - - /** - * @Route("/{id}/edit", name="admin_post_edit") - * @Security("post.isAuthor(user)") - */ - public function edit(Post $post) - { - // ... - } - -.. code-block:: html+twig - - {% if post.isAuthor(app.user) %} - ... - {% endif %} - -.. _best-practices-directly-isGranted: -.. _checking-permissions-without-security: -.. _manually-checking-permissions: - -Checking Permissions without @Security --------------------------------------- - -The above example with ``@Security`` only works because we're using the -:ref:`ParamConverter `, which gives the expression -access to the ``post`` variable. If you don't use this, or have some other -more advanced use-case, you can always do the same security check in PHP:: - - /** - * @Route("/{id}/edit", name="admin_post_edit") - */ - public function edit($id) - { - $post = $this->getDoctrine() - ->getRepository(Post::class) - ->find($id); - - if (!$post) { - throw $this->createNotFoundException(); - } - - if (!$post->isAuthor($this->getUser())) { - $this->denyAccessUnlessGranted('edit', $post); - } - // equivalent code without using the "denyAccessUnlessGranted()" shortcut: - // - // use Symfony\Component\Security\Core\Exception\AccessDeniedException; - // use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface - // - // ... - // - // public function __construct(AuthorizationCheckerInterface $authorizationChecker) { - // $this->authorizationChecker = $authorizationChecker; - // } - // - // ... - // - // if (!$this->authorizationChecker->isGranted('edit', $post)) { - // throw $this->createAccessDeniedException(); - // } - // - // ... - } - -Security Voters ---------------- - -If your security logic is complex and can't be centralized into a method like -``isAuthor()``, you should leverage custom voters. These are much easier than -:doc:`ACLs ` and will give you the flexibility you need in almost -all cases. - -First, create a voter class. The following example shows a voter that implements -the same ``getAuthorEmail()`` logic you used above:: - - namespace App\Security; - - use App\Entity\Post; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; - use Symfony\Component\Security\Core\Authorization\Voter\Voter; - use Symfony\Component\Security\Core\User\UserInterface; - - class PostVoter extends Voter - { - const CREATE = 'create'; - const EDIT = 'edit'; - - private $decisionManager; - - public function __construct(AccessDecisionManagerInterface $decisionManager) - { - $this->decisionManager = $decisionManager; - } - - protected function supports($attribute, $subject) - { - if (!in_array($attribute, [self::CREATE, self::EDIT])) { - return false; - } - - if (!$subject instanceof Post) { - return false; - } - - return true; - } - - protected function voteOnAttribute($attribute, $subject, TokenInterface $token) - { - $user = $token->getUser(); - /** @var Post */ - $post = $subject; // $subject must be a Post instance, thanks to the supports method - - if (!$user instanceof UserInterface) { - return false; - } - - switch ($attribute) { - // if the user is an admin, allow them to create new posts - case self::CREATE: - if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) { - return true; - } - - break; - - // if the user is the author of the post, allow them to edit the posts - case self::EDIT: - if ($user->getEmail() === $post->getAuthorEmail()) { - return true; - } - - break; - } - - return false; - } - } - -If you're using the :ref:`default services.yaml configuration `, -your application will :ref:`autoconfigure ` your security -voter and inject an ``AccessDecisionManagerInterface`` instance into it thanks to -:doc:`autowiring `. - -Now, you can use the voter with the ``@Security`` annotation:: - - /** - * @Route("/{id}/edit", name="admin_post_edit") - * @Security("is_granted('edit', post)") - */ - public function edit(Post $post) - { - // ... - } - -You can also use this directly with the ``security.authorization_checker`` service or -via the even easier shortcut in a controller:: - - /** - * @Route("/{id}/edit", name="admin_post_edit") - */ - public function edit($id) - { - $post = ...; // query for the post - - $this->denyAccessUnlessGranted('edit', $post); - - // use Symfony\Component\Security\Core\Exception\AccessDeniedException; - // use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; - // - // ... - // - // public function __construct(AuthorizationCheckerInterface $authorizationChecker) { - // $this->authorizationChecker = $authorizationChecker; - // } - // - // ... - // - // if (!$this->authorizationChecker->isGranted('edit', $post)) { - // throw $this->createAccessDeniedException(); - // } - // - // ... - } - -Next: :doc:`/best_practices/web-assets` - -.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html -.. _`libsodium`: https://pecl.php.net/package/libsodium diff --git a/best_practices/templates.rst b/best_practices/templates.rst deleted file mode 100644 index 6570690dc56..00000000000 --- a/best_practices/templates.rst +++ /dev/null @@ -1,121 +0,0 @@ -Templates -========= - -When PHP was created 20 years ago, developers loved its simplicity and how -well it blended HTML and dynamic code. But as time passed, other template -languages - like `Twig`_ - were created to make templating even better. - -.. best-practice:: - - Use Twig templating format for your templates. - -Generally speaking, PHP templates are more verbose than Twig templates because -they lack native support for lots of modern features needed by templates, -like inheritance, automatic escaping and named arguments for filters and -functions. - -Twig is the default templating format in Symfony and has the largest community -support of all non-PHP template engines (it's used in high profile projects -such as Drupal 8). - -Template Locations ------------------- - -.. best-practice:: - - Store the application templates in the ``templates/`` directory at the root - of your project. - -Centralizing your templates in a single location simplifies the work of your -designers. In addition, using this directory simplifies the notation used when -referring to templates (e.g. ``$this->render('admin/post/show.html.twig')`` -instead of ``$this->render('@SomeTwigNamespace/Admin/Posts/show.html.twig')``). - -.. best-practice:: - - Use lowercased snake_case for directory and template names. - -This recommendation aligns with Twig best practices, where variables and template -names use lowercased snake_case too (e.g. ``user_profile`` instead of ``userProfile`` -and ``edit_form.html.twig`` instead of ``EditForm.html.twig``). - -.. best-practice:: - - Use a prefixed underscore for partial templates in template names. - -You often want to reuse template code using the ``include`` function to avoid -redundant code. To determine those partials in the filesystem you should -prefix partials and any other template without HTML body or ``extends`` tag -with a single underscore. - -Twig Extensions ---------------- - -.. best-practice:: - - Define your Twig extensions in the ``src/Twig/`` directory. Your - application will automatically detect them and configure them. - -Our application needs a custom ``md2html`` Twig filter so that we can transform -the Markdown contents of each post into HTML. To do this, create a new -``Markdown`` class that will be used later by the Twig extension. It needs -to define one single method to transform Markdown content into HTML:: - - namespace App\Utils; - - class Markdown - { - // ... - - public function toHtml(string $text): string - { - return $this->parser->text($text); - } - } - -Next, create a new Twig extension and define a filter called ``md2html`` using -the ``Twig\TwigFilter`` class. Inject the newly defined ``Markdown`` class in the -constructor of the Twig extension:: - - namespace App\Twig; - - use App\Utils\Markdown; - use Twig\Extension\AbstractExtension; - use Twig\TwigFilter; - - class AppExtension extends AbstractExtension - { - private $parser; - - public function __construct(Markdown $parser) - { - $this->parser = $parser; - } - - public function getFilters() - { - return [ - new TwigFilter('md2html', [$this, 'markdownToHtml'], [ - 'is_safe' => ['html'], - 'pre_escape' => 'html', - ]), - ]; - } - - public function markdownToHtml($content) - { - return $this->parser->toHtml($content); - } - } - -And that's it! - -If you're using the :ref:`default services.yaml configuration `, -you're done! Symfony will automatically know about your new service and tag it to -be used as a Twig extension. - ----- - -Next: :doc:`/best_practices/forms` - -.. _`Twig`: https://twig.symfony.com/ diff --git a/best_practices/tests.rst b/best_practices/tests.rst deleted file mode 100644 index 856ec055271..00000000000 --- a/best_practices/tests.rst +++ /dev/null @@ -1,124 +0,0 @@ -Tests -===== - -Of all the different types of test available, these best practices focus solely -on unit and functional tests. Unit testing allows you to test the input and -output of specific functions. Functional testing allows you to command a -"browser" where you browse to pages on your site, click links, fill out forms -and assert that you see certain things on the page. - -Unit Tests ----------- - -Unit tests are used to test your "business logic", which should live in classes -that are independent of Symfony. For that reason, Symfony doesn't really -have an opinion on what tools you use for unit testing. However, the most -popular tools are `PHPUnit`_ and `PHPSpec`_. - -Functional Tests ----------------- - -Creating really good functional tests can be tough so some developers skip -these completely. Don't skip the functional tests! By defining some *simple* -functional tests, you can quickly spot any big errors before you deploy them: - -.. best-practice:: - - Define a functional test that at least checks if your application pages - are successfully loading. - -:ref:`PHPUnit data providers ` help you implement -functional tests:: - - // tests/ApplicationAvailabilityFunctionalTest.php - namespace App\Tests; - - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - - class ApplicationAvailabilityFunctionalTest extends WebTestCase - { - /** - * @dataProvider urlProvider - */ - public function testPageIsSuccessful($url) - { - $client = self::createClient(); - $client->request('GET', $url); - - $this->assertTrue($client->getResponse()->isSuccessful()); - } - - public function urlProvider() - { - yield ['/']; - yield ['/posts']; - yield ['/post/fixture-post-1']; - yield ['/blog/category/fixture-category']; - yield ['/archives']; - // ... - } - } - -This code checks that all the given URLs load successfully, which means that -their HTTP response status code is between ``200`` and ``299``. This may -not look that useful, but given how little effort this took, it's worth -having it in your application. - -In computer software, this kind of test is called `smoke testing`_ and consists -of *"preliminary testing to reveal simple failures severe enough to reject a -prospective software release"*. - -Hardcode URLs in a Functional Test -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some of you may be asking why the previous functional test doesn't use the URL -generator service: - -.. best-practice:: - - Hardcode the URLs used in the functional tests instead of using the URL - generator. - -Consider the following functional test that uses the ``router`` service to -generate the URL of the tested page:: - - // ... - private $router; // consider that this holds the Symfony router service - - public function testBlogArchives() - { - $client = self::createClient(); - $url = $this->router->generate('blog_archives'); - $client->request('GET', $url); - - // ... - } - -This will work, but it has one *huge* drawback. If a developer mistakenly -changes the path of the ``blog_archives`` route, the test will still pass, -but the original (old) URL won't work! This means that any bookmarks for -that URL will be broken and you'll lose any search engine page ranking. - -Testing JavaScript Functionality --------------------------------- - -The built-in functional testing client is great, but it can't be used to -test any JavaScript behavior on your pages. If you need to test this, consider -using the `Mink`_ library from within PHPUnit. - -If you have a heavy JavaScript frontend, you should consider using pure -JavaScript-based testing tools. - -Learn More about Functional Tests ---------------------------------- - -Consider using the `HautelookAliceBundle`_ to generate real-looking data for -your test fixtures using `Faker`_ and `Alice`_. - -.. _`PHPUnit`: https://phpunit.de/ -.. _`PHPSpec`: https://www.phpspec.net/ -.. _`smoke testing`: https://en.wikipedia.org/wiki/Smoke_testing_(software) -.. _`Mink`: http://mink.behat.org -.. _`HautelookAliceBundle`: https://github.com/hautelook/AliceBundle -.. _`Faker`: https://github.com/fzaninotto/Faker -.. _`Alice`: https://github.com/nelmio/alice diff --git a/best_practices/web-assets.rst b/best_practices/web-assets.rst deleted file mode 100644 index 271a1fa3eeb..00000000000 --- a/best_practices/web-assets.rst +++ /dev/null @@ -1,34 +0,0 @@ -Web Assets -========== - -Web assets are things like CSS, JavaScript and image files that make the -frontend of your site look and work great. - -.. best-practice:: - - Store your assets in the ``assets/`` directory at the root of your project. - -Your designers' and front-end developers' lives will be much easier if all the -application assets are in one central location. - -.. best-practice:: - - Use `Webpack Encore`_ to compile, combine and minimize web assets. - -`Webpack`_ is the leading JavaScript module bundler that compiles, transforms -and packages assets for usage in a browser. Webpack Encore is a JavaScript -library that gets rid of most of Webpack complexity without hiding any of its -features or distorting its usage and philosophy. - -Webpack Encore was designed to bridge the gap between Symfony applications and -the JavaScript-based tools used in modern web applications. Check out the -`official Webpack Encore documentation`_ to learn more about all the available -features. - ----- - -Next: :doc:`/best_practices/tests` - -.. _`Webpack Encore`: https://github.com/symfony/webpack-encore -.. _`Webpack`: https://webpack.js.org/ -.. _`official Webpack Encore documentation`: https://symfony.com/doc/current/frontend.html diff --git a/contributing/documentation/standards.rst b/contributing/documentation/standards.rst index 0cfad8880ed..8ae0611acdd 100644 --- a/contributing/documentation/standards.rst +++ b/contributing/documentation/standards.rst @@ -53,7 +53,7 @@ Code Examples as well as the `Twig Coding Standards`_; * The code examples should look real for a web application context. Avoid abstract or trivial examples (``foo``, ``bar``, ``demo``, etc.); -* The code should follow the :doc:`Symfony Best Practices `. +* The code should follow the :doc:`Symfony Best Practices `. * Use ``Acme`` when the code requires a vendor name; * Use ``example.com`` as the domain of sample URLs and ``example.org`` and ``example.net`` when additional domains are required. All of these domains are diff --git a/forms.rst b/forms.rst index b902e53ba3e..30703f860e4 100644 --- a/forms.rst +++ b/forms.rst @@ -157,10 +157,10 @@ added a submit button with a custom label for submitting the form to the server. Creating Form Classes ~~~~~~~~~~~~~~~~~~~~~ -Symfony recommends to :doc:`create thin controllers `. -That's why it's better to move complex forms to dedicated classes instead of -defining them in controller actions. Besides, forms defined in classes can be -reused in multiple actions and services. +Symfony recommends to put as little logic as possible in controllers. That's why +it's better to move complex forms to dedicated classes instead of defining them +in controller actions. Besides, forms defined in classes can be reused in +multiple actions and services. Form classes are :ref:`form types ` that implement :class:`Symfony\\Component\\Form\\FormTypeInterface`. However, it's better to @@ -245,6 +245,8 @@ the ``data_class`` option by adding the following to your form type class:: } } +.. _rendering-forms: + Rendering Forms --------------- diff --git a/index.rst b/index.rst index 7fc90780628..ac293625cb9 100644 --- a/index.rst +++ b/index.rst @@ -31,6 +31,7 @@ Topics .. toctree:: :maxdepth: 1 + best_practices bundles cache console @@ -60,16 +61,6 @@ Topics web_link workflow -Best Practices --------------- - -.. toctree:: - :hidden: - - best_practices/index - -Read the :doc:`Official Best Practices `. - Components ---------- diff --git a/routing.rst b/routing.rst index f9405408555..7d48be61c17 100644 --- a/routing.rst +++ b/routing.rst @@ -17,8 +17,8 @@ Creating Routes Routes can be configured in YAML, XML, PHP or using annotations. All formats provide the same features and performance, so choose your favorite. -:doc:`Symfony recommends annotations ` because it's -convenient to put the route and controller in the same place. +:ref:`Symfony recommends annotations ` +because it's convenient to put the route and controller in the same place. Creating Routes as Annotations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/templates.rst b/templates.rst index d92357954a0..73ae5887335 100644 --- a/templates.rst +++ b/templates.rst @@ -625,6 +625,8 @@ To avoid leaking sensitive information, the ``dump()`` function/tag is only available in the ``dev`` and ``test`` :ref:`configuration environments `. If you try to use it in the ``prod`` environment, you will see a PHP error. +.. _templates-reuse-contents: + Reusing Template Contents ------------------------- diff --git a/testing.rst b/testing.rst index e772a1eb04d..aab91beb2ca 100644 --- a/testing.rst +++ b/testing.rst @@ -1054,7 +1054,6 @@ Learn more :glob: testing/* - /best_practices/tests /components/dom_crawler /components/css_selector