diff --git a/best_practices/business-logic.rst b/best_practices/business-logic.rst index 96188910943..e59705e34de 100644 --- a/best_practices/business-logic.rst +++ b/best_practices/business-logic.rst @@ -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 ` 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 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 @@ -42,34 +52,15 @@ 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 `, - 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 `, +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:: @@ -77,21 +68,17 @@ use the 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); @@ -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 ------------------------- diff --git a/best_practices/controllers.rst b/best_practices/controllers.rst index 6eb25d03c4f..bf563380402 100644 --- a/best_practices/controllers.rst +++ b/best_practices/controllers.rst @@ -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. @@ -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 --------------------- @@ -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 `: .. best-practice:: @@ -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(); @@ -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) { // ... } diff --git a/best_practices/creating-the-project.rst b/best_practices/creating-the-project.rst index 5bdfbd08693..5784b2fd343 100644 --- a/best_practices/creating-the-project.rst +++ b/best_practices/creating-the-project.rst @@ -4,16 +4,23 @@ Creating the Project Installing Symfony ------------------ -In the past, Symfony projects were created with `Composer`_, the dependency manager -for PHP applications. However, the current recommendation is to use the **Symfony -Installer**, which has to be installed before creating your first project. +.. best-practice:: + + Use Composer and Symfony Flex to create and manage Symfony applications. + +`Composer`_ is the package manager used by modern PHP application 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 Installer to create new Symfony-based projects. + Use the Symfony Skeleton to create new Symfony-based projects. -Read the :doc:`/setup` article learn how to install and use the Symfony -Installer. +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: @@ -21,36 +28,21 @@ Installer. Creating the Blog Application ----------------------------- -Now that everything is correctly set up, you can create a new project based on -Symfony. In your command console, browse to a directory where you have permission -to create files and execute the following commands: +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/ - $ symfony new blog - - # Windows - c:\> cd projects/ - c:\projects\> php symfony new blog - -.. note:: - - If the installer doesn't work for you or doesn't output anything, make sure - that the `Phar extension`_ is installed and enabled on your computer. + $ 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. In addition, -the installer checks if your system meets the technical requirements to execute -Symfony applications. If not, you'll see the list of changes needed to meet those -requirements. +project based on the most recent stable Symfony version available. .. tip:: - Symfony releases are digitally signed for security reasons. If you want to - verify the integrity of your Symfony installation, take a look at the - `public checksums repository`_ and follow `these steps`_ to verify the - signatures. + The technical requirements to run Symfony are simple. If you want to check + if your system meets those requirements, read :doc:`/reference/requirements`. Structuring the Application --------------------------- @@ -61,47 +53,29 @@ number of files and directories generated automatically: .. code-block:: text blog/ - ├─ app/ - │ ├─ config/ - │ └─ Resources/ - ├─ bin + ├─ bin/ │ └─ console + ├─ config/ + └─ public/ + │ └─ index.php ├─ src/ - │ └─ AppBundle/ + │ └─ Kernel.php ├─ var/ │ ├─ cache/ - │ ├─ logs/ - │ └─ sessions/ - ├─ tests/ - │ └─ AppBundle/ - ├─ vendor/ - └─ web/ + │ └─ log/ + └─ vendor/ This file and directory hierarchy is the convention proposed by Symfony to -structure your applications. The recommended purpose of each directory is the -following: - -* ``app/config/``, stores all the configuration defined for any environment; -* ``app/Resources/``, stores all the templates and the translation files for the - application; -* ``src/AppBundle/``, stores the Symfony specific code (controllers and routes), - your domain code (e.g. Doctrine classes) and all your business logic; -* ``var/cache/``, stores all the cache files generated by the application; -* ``var/log/``, stores all the log files generated by the application; -* ``var/sessions/``, stores all the session files generated by the application; -* ``tests/AppBundle/``, stores the automatic tests (e.g. Unit tests) of the - application. -* ``vendor/``, this is the directory where Composer installs the application's - dependencies and you should never modify any of its contents; -* ``web/``, stores all the front controller files and all the web assets, such - as stylesheets, JavaScript files and images. +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 -apps use bundles to divide their code into logical features: UserBundle, +apps 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 @@ -111,64 +85,12 @@ ProductBundle, then there's no advantage to having two separate bundles. .. best-practice:: - Create only one bundle called AppBundle for your application logic. - -Implementing a single AppBundle bundle in your projects will make your code -more concise and easier to understand. - -.. note:: - - There is no need to prefix the AppBundle with your own vendor (e.g. - AcmeAppBundle), because this application bundle is never going to be - shared. - -.. note:: - - Another reason to create a new bundle is when you're overriding something - in a vendor's bundle (e.g. a controller). See :doc:`/bundles/inheritance`. - -All in all, this is the typical directory structure of a Symfony application -that follows these best practices: - -.. code-block:: text - - blog/ - ├─ app/ - │ ├─ config/ - │ └─ Resources/ - ├─ bin/ - │ └─ console - ├─ src/ - │ └─ AppBundle/ - ├─ tests/ - │ └─ AppBundle/ - ├─ var/ - │ ├─ cache/ - │ ├─ logs/ - └─ sessions/ - ├─ vendor/ - └─ web/ - ├─ app.php - └─ app_dev.php - -.. tip:: - - If your Symfony installation doesn't come with a pre-generated AppBundle, - you can generate it by hand executing this command: - - .. code-block:: terminal - - $ php bin/console generate:bundle --namespace=AppBundle --dir=src --format=annotation --no-interaction - -Extending the Directory Structure ---------------------------------- + Don't create any bundle to organize your application logic. -If your project or infrastructure requires some changes to the default directory -structure of Symfony, you can -:doc:`override the location of the main directories `: -``cache/``, ``logs/`` and ``web/``. +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. .. _`Composer`: https://getcomposer.org/ -.. _`Phar extension`: http://php.net/manual/en/intro.phar.php -.. _`public checksums repository`: https://github.com/sensiolabs/checksums -.. _`these steps`: http://fabien.potencier.org/signing-project-releases.html +.. _`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 index 33d34e88753..17c3a3ebe16 100644 --- a/best_practices/forms.rst +++ b/best_practices/forms.rst @@ -12,10 +12,10 @@ Building Forms 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:: +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; @@ -42,9 +42,9 @@ form in its own PHP class:: public function configureOptions(OptionsResolver $resolver) { - $resolver->setDefaults(array( + $resolver->setDefaults([ 'data_class' => Post::class, - )); + ]); } } @@ -59,7 +59,7 @@ To use the class, use ``createForm()`` and pass the fully qualified class name:: use App\Form\PostType; // ... - public function newAction(Request $request) + public function new(Request $request) { $post = new Post(); $form = $this->createForm(PostType::class, $post); @@ -67,14 +67,6 @@ To use the class, use ``createForm()`` and pass the fully qualified class name:: // ... } -Registering Forms as Services -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also :ref:`register your form type as a service `. -This is only needed if your form type requires some dependencies to be injected -by the container, otherwise it is unnecessary overhead and therefore *not* -recommended to do this for all form type classes. - Form Button Configuration ------------------------- @@ -98,7 +90,7 @@ scope of that form: { $builder // ... - ->add('save', SubmitType::class, array('label' => 'Create Post')) + ->add('save', SubmitType::class, ['label' => 'Create Post']) ; } @@ -121,14 +113,14 @@ some developers configure form buttons in the controller:: { // ... - public function newAction(Request $request) + public function new(Request $request) { $post = new Post(); $form = $this->createForm(PostType::class, $post); - $form->add('submit', SubmitType::class, array( + $form->add('submit', SubmitType::class, [ 'label' => 'Create', - 'attr' => array('class' => 'btn btn-default pull-right'), - )); + 'attr' => ['class' => 'btn btn-default pull-right'], + ]); // ... } @@ -144,8 +136,7 @@ view layer: {{ form_start(form) }} {{ form_widget(form) }} - + {{ form_end(form) }} Rendering the Form @@ -161,7 +152,7 @@ all of the fields: .. code-block:: html+twig - {{ form_start(form, {'attr': {'class': 'my-form-class'} }) }} + {{ form_start(form, {attr: {class: 'my-form-class'} }) }} {{ form_widget(form) }} {{ form_end(form) }} @@ -177,7 +168,7 @@ Handling a form submit usually follows a similar template: .. code-block:: php - public function newAction(Request $request) + public function new(Request $request) { // build the form ... @@ -188,22 +179,16 @@ Handling a form submit usually follows a similar template: $em->persist($post); $em->flush(); - return $this->redirect($this->generateUrl( - 'admin_post_show', - array('id' => $post->getId()) - )); + return $this->redirectToRoute('admin_post_show', [ + 'id' => $post->getId() + ]); } // render the template } -There are really only two notable things here. First, we recommend that you -use a single action for both rendering the form and handling the form submit. -For example, you *could* have a ``newAction()`` that *only* renders the form -and a ``createAction()`` that *only* processes the form submit. Both those -actions will be almost identical. So it's much simpler to let ``newAction()`` -handle everything. - -Second, is it required to call ``$form->isSubmitted()`` in the ``if`` statement -before calling ``isValid()``. Calling ``isValid()`` with an unsubmitted form -is deprecated since version 3.2 and will throw an exception in 4.0. +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. diff --git a/best_practices/i18n.rst b/best_practices/i18n.rst index 39473373df1..362af65bfa0 100644 --- a/best_practices/i18n.rst +++ b/best_practices/i18n.rst @@ -3,20 +3,18 @@ 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 enabled before using it. To do this, uncomment the -following ``translator`` configuration option and set your application locale: +feature that needs to be installed before using it (``composer require translation``). -.. code-block:: yaml +Translation Source File Location +-------------------------------- + +.. best-practice:: - # app/config/config.yml - framework: - # ... - translator: { fallbacks: ['%locale%'] } + Store the translation files in the ``translations/`` directory at the root + of your project. - # app/config/parameters.yml - parameters: - # ... - locale: en +Your translators' lives will be much easier if all the application translations +are in one central location. Translation Source File Format ------------------------------ @@ -42,21 +40,6 @@ XLIFF notes allow you to define that context. viewing and editing these translation files. It also has advanced extractors that can read your project and automatically update the XLIFF files. -Translation Source File Location --------------------------------- - -.. best-practice:: - - Store the translation files in the ``app/Resources/translations/`` - directory. - -Traditionally, Symfony developers have created these files in the -``Resources/translations/`` directory of each bundle. But since the -``app/Resources/`` directory is considered the global location for the -application's resources, storing translations in ``app/Resources/translations/`` -centralizes them *and* gives them priority over any other translation file. -This let's you override translations defined in third-party bundles. - Translation Keys ---------------- @@ -64,8 +47,8 @@ Translation Keys 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 +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 @@ -80,7 +63,7 @@ English in the application would be: .. code-block:: xml - + diff --git a/best_practices/introduction.rst b/best_practices/introduction.rst index 4260b79fd1a..c0507a08af6 100644 --- a/best_practices/introduction.rst +++ b/best_practices/introduction.rst @@ -58,8 +58,8 @@ 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! -Start with :doc:`The Quick Tour ` tutorial first. +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 @@ -69,14 +69,13 @@ what you already know. The Application --------------- -In addition to this guide, a sample application has been developed with all these -best practices in mind. This project, called the Symfony Demo application, can -be obtained through the Symfony Installer. First, `download and install`_ the -installer and then execute this command to download the demo 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 - $ symfony demo + $ 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 @@ -87,9 +86,10 @@ 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 -should not refactor your existing applications to comply with these best -practices**. The reasons for not doing it are various: +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; @@ -99,4 +99,4 @@ practices**. The reasons for not doing it are various: your tests or adding features that provide real value to the end users. .. _`Fabien Potencier`: https://connect.sensiolabs.com/profile/fabpot -.. _`download and install`: https://symfony.com/download +.. _`Symfony Demo`: https://github.com/symfony/demo diff --git a/best_practices/security.rst b/best_practices/security.rst index ece701c78bf..6135748fa3e 100644 --- a/best_practices/security.rst +++ b/best_practices/security.rst @@ -6,8 +6,7 @@ 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. +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. @@ -30,9 +29,9 @@ site (or maybe nearly *all* sections), use the ``access_control`` area. .. best-practice:: - Use the ``bcrypt`` encoder for encoding your users' passwords. + Use the ``bcrypt`` encoder for hashing your users' passwords. -If your users have a password, then we recommend encoding it using the ``bcrypt`` +If your users have a password, then we recommend hashing it using the ``bcrypt`` encoder, instead of the traditional SHA-512 hashing encoder. The main advantages of ``bcrypt`` are the inclusion of a *salt* value to protect against rainbow table attacks, and its adaptive nature, which allows to make it slower to @@ -83,17 +82,15 @@ service directly. * 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. + * 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 or with ACL. +with a custom security voter: .. best-practice:: - * For fine-grained restrictions, define a custom security voter; - * For restricting access to *any* object by *any* user via an admin - interface, use the Symfony ACL. + Define a custom security voter to implement fine-grained restrictions. .. _best-practices-security-annotation: @@ -119,7 +116,7 @@ Using ``@Security``, this looks like: * @Route("/new", name="admin_post_new") * @Security("has_role('ROLE_ADMIN')") */ - public function newAction() + public function new() { // ... } @@ -142,7 +139,7 @@ method on the ``Post`` object: * @Route("/{id}/edit", name="admin_post_edit") * @Security("user.getEmail() == post.getAuthorEmail()") */ - public function editAction(Post $post) + public function edit(Post $post) { // ... } @@ -197,7 +194,7 @@ Now you can reuse this method both in the template and in the security expressio * @Route("/{id}/edit", name="admin_post_edit") * @Security("post.isAuthor(user)") */ - public function editAction(Post $post) + public function edit(Post $post) { // ... } @@ -225,7 +222,7 @@ more advanced use-case, you can always do the same security check in PHP: /** * @Route("/{id}/edit", name="admin_post_edit") */ - public function editAction($id) + public function edit($id) { $post = $this->getDoctrine() ->getRepository(Post::class) @@ -253,10 +250,10 @@ more advanced use-case, you can always do the same security check in PHP: 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 an order -of magnitude easier than :doc:`ACLs ` and will give -you the flexibility you need in almost all cases. +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: @@ -276,9 +273,6 @@ the same ``getAuthorEmail()`` logic you used above: const CREATE = 'create'; const EDIT = 'edit'; - /** - * @var AccessDecisionManagerInterface - */ private $decisionManager; public function __construct(AccessDecisionManagerInterface $decisionManager) @@ -288,7 +282,7 @@ the same ``getAuthorEmail()`` logic you used above: protected function supports($attribute, $subject) { - if (!in_array($attribute, array(self::CREATE, self::EDIT))) { + if (!in_array($attribute, [self::CREATE, self::EDIT])) { return false; } @@ -310,15 +304,16 @@ the same ``getAuthorEmail()`` logic you used above: } switch ($attribute) { + // if the user is an admin, allow them to create new posts case self::CREATE: - // if the user is an admin, allow them to create new posts - if ($this->decisionManager->decide($token, array('ROLE_ADMIN'))) { + 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 the user is the author of the post, allow them to edit the posts if ($user->getEmail() === $post->getAuthorEmail()) { return true; } @@ -343,7 +338,7 @@ Now, you can use the voter with the ``@Security`` annotation: * @Route("/{id}/edit", name="admin_post_edit") * @Security("is_granted('edit', post)") */ - public function editAction(Post $post) + public function edit(Post $post) { // ... } @@ -356,7 +351,7 @@ via the even easier shortcut in a controller: /** * @Route("/{id}/edit", name="admin_post_edit") */ - public function editAction($id) + public function edit($id) { $post = ...; // query for the post diff --git a/best_practices/templates.rst b/best_practices/templates.rst index 0a45d7b7351..9f3d1feaac7 100644 --- a/best_practices/templates.rst +++ b/best_practices/templates.rst @@ -9,7 +9,7 @@ languages - like `Twig`_ - were created to make templating even better. Use Twig templating format for your templates. -Generally speaking, PHP templates are much more verbose than Twig templates because +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. @@ -18,93 +18,65 @@ 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). -In addition, Twig is the only template format with guaranteed support in Symfony -3.0. As a matter of fact, PHP may be removed from the officially supported -template engines. - Template Locations ------------------ .. best-practice:: - Store all your application's templates in ``app/Resources/views/`` directory. - -Traditionally, Symfony developers stored the application templates in the -``Resources/views/`` directory of each bundle. Then they used the Twig namespaced -path to refer to them (e.g. ``@AcmeDemo/Default/index.html.twig``). - -But for the templates used in your application, it's much more convenient -to store them in the ``app/Resources/views/`` directory. For starters, this -drastically simplifies their logical names: + Store the application templates in the ``templates/`` directory at the root + of your project. -============================================ ================================== -Templates Stored inside Bundles Templates Stored in ``app/`` -============================================ ================================== -``@AcmeDemo/index.html.twig`` ``index.html.twig`` -``@AcmeDemo/Default/index.html.twig`` ``default/index.html.twig`` -``@AcmeDemo/Default/subdir/index.html.twig`` ``default/subdir/index.html.twig`` -============================================ ================================== - -Another advantage is that centralizing your templates simplifies the work -of your designers. They don't need to look for templates in lots of directories -scattered through lots of bundles. +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``). + Twig Extensions --------------- .. best-practice:: - Define your Twig extensions in the ``AppBundle/Twig/`` directory. Your + 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, first, install the excellent `Parsedown`_ Markdown parser as -a new dependency of the project: - -.. code-block:: terminal - - $ composer require erusev/parsedown - -Then, create a new ``Markdown`` class that will be used later by the Twig -extension. It just needs to define one single method to transform -Markdown content into HTML:: +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 just needs +to define one single method to transform Markdown content into HTML:: namespace App\Utils; class Markdown { - private $parser; - - public function __construct() - { - $this->parser = new \Parsedown(); - } + // ... - public function toHtml($text) + public function toHtml(string $text): string { - $html = $this->parser->text($text); - - return $html; + return $this->parser->text($text); } } -Next, create a new Twig extension and define a new filter called ``md2html`` -using the ``Twig_SimpleFilter`` class. Inject the newly defined ``Markdown`` -class in the constructor of the Twig extension: +Next, create a new Twig extension and define a filter called ``md2html`` using +the ``TwigFilter`` class. Inject the newly defined ``Markdown`` class in the +constructor of the Twig extension: .. code-block:: php namespace App\Twig; use App\Utils\Markdown; + use Twig\Extension\AbstractExtension; + use Twig\TwigFilter; - class AppExtension extends \Twig_Extension + class AppExtension extends AbstractExtension { private $parser; @@ -115,29 +87,23 @@ class in the constructor of the Twig extension: public function getFilters() { - return array( - new \Twig_SimpleFilter( - 'md2html', - array($this, 'markdownToHtml'), - array('is_safe' => array('html'), 'pre_escape' => 'html') - ), - ); + return [ + new TwigFilter('md2html', [$this, 'markdownToHtml'], [ + 'is_safe' => ['html'], + 'pre_escape' => 'html', + ]), + ]; } public function markdownToHtml($content) { return $this->parser->toHtml($content); } - - public function getName() - { - return 'app_extension'; - } } And that's it! -If you're using the :ref:`default services.yml configuration `, +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. diff --git a/best_practices/tests.rst b/best_practices/tests.rst index ace92cb5fe5..621ab6171b6 100644 --- a/best_practices/tests.rst +++ b/best_practices/tests.rst @@ -1,10 +1,11 @@ Tests ===== -Roughly speaking, there are two types of test. 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. +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 ---------- @@ -48,14 +49,12 @@ A functional test can be as easy as this:: public function urlProvider() { - return array( - array('/'), - array('/posts'), - array('/post/fixture-post-1'), - array('/blog/category/fixture-category'), - array('/archives'), - // ... - ); + yield ['/']; + yield ['/posts']; + yield ['/post/fixture-post-1']; + yield ['/blog/category/fixture-category']; + yield ['/archives']; + // ... } } @@ -84,10 +83,13 @@ generate the URL of the tested page: .. code-block:: php + // ... + private $router; // consider that this holds the Symfony router service + public function testBlogArchives() { $client = self::createClient(); - $url = $client->getContainer()->get('router')->generate('blog_archives'); + $url = $this->router->generate('blog_archives'); $client->request('GET', $url); // ... @@ -105,7 +107,7 @@ 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. -Of course, if you have a heavy JavaScript frontend, you should consider using +Of course, if you have a heavy JavaScript front-end, you should consider using pure JavaScript-based testing tools. Learn More about Functional Tests diff --git a/best_practices/web-assets.rst b/best_practices/web-assets.rst index c7ab18e2114..dbff85d34e6 100644 --- a/best_practices/web-assets.rst +++ b/best_practices/web-assets.rst @@ -2,97 +2,29 @@ Web Assets ========== Web assets are things like CSS, JavaScript and image files that make the -frontend of your site look and work great. Symfony developers have traditionally -stored these assets in the ``Resources/public/`` directory of each bundle. +frontend of your site look and work great. .. best-practice:: - Store your assets in the ``public/`` directory. + Store your assets in the ``assets/`` directory at the root of your project. -Scattering your web assets across tens of different bundles makes it more -difficult to manage them. Your designers' lives will be much easier if all -the application assets are in one location. - -Templates also benefit from centralizing your assets, because the links are -much more concise: - -.. code-block:: html+twig - - - - - {# ... #} - - - - -.. note:: - - Keep in mind that ``public/`` is a public directory and that anything stored - here will be publicly accessible, including all the original asset files - (e.g. Sass, LESS and CoffeeScript files). - -Using Assetic -------------- - -.. include:: /assetic/_standard_edition_warning.rst.inc - -These days, you probably can't simply create static CSS and JavaScript files -and include them in your template. Instead, you'll probably want to combine -and minify these to improve client-side performance. You may also want to -use LESS or Sass (for example), which means you'll need some way to process -these into CSS files. - -A lot of tools exist to solve these problems, including pure-frontend (non-PHP) -tools like GruntJS. +Your designers' and front-end developers' lives will be much easier if all the +application assets are in one central location. .. best-practice:: - Use Assetic to compile, combine and minimize web assets, unless you're - comfortable with frontend tools like GruntJS. - -:doc:`Assetic ` is an asset manager capable -of compiling assets developed with a lot of different frontend technologies -like LESS, Sass and CoffeeScript. Combining all your assets with Assetic is a -matter of wrapping all the assets with a single Twig tag: - -.. code-block:: html+twig - - {% stylesheets - 'css/bootstrap.min.css' - 'css/main.css' - filter='cssrewrite' output='css/compiled/app.css' %} - - {% endstylesheets %} - - {# ... #} - - {% javascripts - 'js/jquery.min.js' - 'js/bootstrap.min.js' - output='js/compiled/app.js' %} - - {% endjavascripts %} - -Frontend-Based Applications ---------------------------- - -Recently, frontend technologies like AngularJS have become pretty popular -for developing frontend web applications that talk to an API. - -If you are developing an application like this, you should use the tools -that are recommended by the technology, such as Bower and GruntJS. You should -develop your frontend application separately from your Symfony backend (even -separating the repositories if you want). + Use `Webpack Encore`_ to compile, combine and minimize web assets. -Learn More about Assetic ------------------------- +`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. -Assetic can also minimize CSS and JavaScript assets -:doc:`using UglifyCSS/UglifyJS ` to speed up your -websites. You can even :doc:`compress images ` -with Assetic to reduce their size before serving them to the user. Check out -the `official Assetic documentation`_ to learn more about all the available +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. -.. _`official Assetic documentation`: https://github.com/kriswallsmith/assetic +.. _`Webpack Encore`: https://github.com/symfony/webpack-encore +.. _`Webpack`: https://webpack.js.org/ +.. _`official Webpack Encore documentation`: https://symfony.com/doc/current/frontend.html