diff --git a/_exts b/_exts index 03bc1c601..e58edd22d 160000 --- a/_exts +++ b/_exts @@ -1 +1 @@ -Subproject commit 03bc1c60172a280619e3476f22b111b4a187895d +Subproject commit e58edd22d16cb247267025d557410dcbfa5fa959 diff --git a/best_practices/business-logic.rst b/best_practices/business-logic.rst new file mode 100644 index 000000000..fe7364ec2 --- /dev/null +++ b/best_practices/business-logic.rst @@ -0,0 +1,344 @@ +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 everything inside the ``AppBundle``. +Inside here, you can create whatever directories you want to organize things: + +.. code-block:: text + + symfoy2-project/ + ├─ app/ + ├─ src/ + │ └─ AppBundle/ + │ └─ Utils/ + │ └─ MyClass.php + ├─ vendor/ + └─ web/ + +Storing Classes Outside of the Bundle? +-------------------------------------- + +But there's no technical reason for putting business logic inside of a bundle. +If you like, you can create your own namespace inside the ``src/`` directory +and put things there: + +.. code-block:: text + + symfoy2-project/ + ├─ app/ + ├─ src/ + │ ├─ Acme/ + │ │ └─ Utils/ + │ │ └─ MyClass.php + │ └─ AppBundle/ + ├─ vendor/ + └─ web/ + +.. tip:: + + The recommended approach of using the ``AppBundle`` directory is for + simplicity. If you're advanced enough to know what needs to live in + a bundle and what can live outside of one, then feel free to do that. + +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. + +Let's, create a new ``Slugger`` class inside ``src/AppBundle/Utils/`` and +add the following ``slugify()`` method: + +.. code-block:: php + + // src/AppBundle/Utils/Slugger.php + namespace AppBundle\Utils; + + class Slugger + { + public function slugify($string) + { + return preg_replace( + '/[^a-z0-9]/', '-', strtolower(trim(strip_tags($string))) + ); + } + } + +Next, define a new service for that class. + +.. code-block:: yaml + + # app/config/services.yml + services: + # keep your service names short + slugger: + class: AppBundle\Utils\Slugger + +Traditionally, the naming convention for a service involved following the +class name and location to avoid name collisions. Thus, the service +*would have been* called ``app.utils.slugger``. But by using short service names, +your code will be easier to read and use. + +.. best-practice:: + + The name of your application's services should be as short as possible, + ideally just one simple word. + +Now you can use the custom slugger in any controller class, such as the +``AdminController``: + +.. code-block:: php + + public function createAction(Request $request) + { + // ... + + if ($form->isSubmitted() && $form->isValid()) { + $slug = $this->get('slugger')->slugify($post->getTitle())); + $post->setSlug($slug); + + // ... + } + } + +Service Format: YAML +-------------------- + +In the previous section, YAML was used to define the service. + +.. best-practice:: + + Use the YAML format to define 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. 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 + + # app/config/services.yml + + # service definition with class namespace as parameter + parameters: + slugger.class: AppBundle\Utils\Slugger + + services: + 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 +------------------------- + +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 of strategy you want for this. + +In practice, many Symfony applications rely on the independent +`Doctrine project`_ to define their model using entities and repositories. +Just like with business logic, we recommend storing Doctrine entities in +the ``AppBundle`` + +The three entities defined by our sample blog application are a good example: + +.. code-block:: text + + symfony2-project/ + ├─ ... + └─ src/ + └─ AppBundle/ + └─ Entity/ + ├─ Comment.php + ├─ Post.php + └─ User.php + +.. tip:: + + If you're more advanced, you can of course store them under your own + namespace in ``src/``. + +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: + +.. code-block:: php + + namespace AppBundle\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Doctrine\Common\Collections\ArrayCollection; + + /** + * @ORM\Entity + */ + class Post + { + const NUM_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="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:: bash + + $ composer require "doctrine/doctrine-fixtures-bundle" + +Then, enable the bundle in ``AppKernel.php``, but only for the ``dev`` and +``test`` environments: + +.. code-block:: php + + use Symfony\Component\HttpKernel\Kernel; + + class AppKernel extends Kernel + { + public function registerBundles() + { + $bundles = array( + // ... + ); + + if (in_array($this->getEnvironment(), array('dev', 'test'))) { + // ... + $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(), + } + + return $bundles; + } + + // ... + } + +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:: bash + + $ php app/console doctrine:fixtures:load + + Careful, database will be purged. Do you want to continue Y/N ? Y + > purging database + > loading AppBundle\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 +`the Symfony Code 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. + +.. _`full definition`: http://en.wikipedia.org/wiki/Business_logic +.. _`Toran Proxy`: https://toranproxy.com/ +.. _`Composer`: https://getcomposer.org/ +.. _`MVC architecture`: http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller +.. _`Doctrine project`: http://www.doctrine-project.org/ +.. _`fixture class`: http://symfony.com/doc/master/bundles/DoctrineFixturesBundle/index.html#writing-simple-fixtures +.. _`PSR-1`: http://www.php-fig.org/psr/psr-1/ +.. _`PSR-2`: http://www.php-fig.org/psr/psr-2/ +.. _`the Symfony Code Standards`: http://symfony.com/doc/current/contributing/code/standards.html +.. _`PHP-CS-Fixer`: https://github.com/fabpot/PHP-CS-Fixer diff --git a/best_practices/configuration.rst b/best_practices/configuration.rst new file mode 100644 index 000000000..ab340bbe7 --- /dev/null +++ b/best_practices/configuration.rst @@ -0,0 +1,187 @@ +Configuration +============= + +La configuration implique généralement différentes parties de l'application (comme +l'infrastructure et la sécurité) et différents environnements (développement, production). +C'est pourquoi Symfony recommande de diviser la configuration de l'application en trois +parties. + +Configuration liée à l'infrastructure +------------------------------------- + +.. best-practice:: + + Définissez les options de configuration liée à l'infrastructure dans + le fichier ``app/config/parameters.yml``. + +Le fichier par défaut ``parameters.yml`` suit cette recommandation et défini les +options relatives à la base de données et au serveur de mail : + +.. code-block:: yaml + + # app/config/parameters.yml + parameters: + database_driver: pdo_mysql + database_host: 127.0.0.1 + database_port: ~ + database_name: symfony + database_user: root + database_password: ~ + + mailer_transport: smtp + mailer_host: 127.0.0.1 + mailer_user: ~ + mailer_password: ~ + + # ... + +Ces options ne sont pas définies dans le fichier ``app/config/config.yml`` car +elles n'ont pas de rapport avec le comportement de l'application. En d'autres termes, +votre application ne se soucie pas de l'emplacement de la base de données ou des +droits permettant d'y avoir accès, tant que la base de données est bien configurée. + +Paramètres standards +~~~~~~~~~~~~~~~~~~~~ + +.. best-practice:: + + Définissez tous les paramètres de votre application dans le fichier + ``app/config/parameters.yml.dist``. + +Depuis la version 2.3, Symfony inclus un fichier de configuration appelé +``parameters.yml.dist``, qui stocke la liste des paramètres de configuration +standard de votre application. + +Chaque fois qu'un nouveau paramètre de configuration est défini pour votre application, +vous devriez également l'ajouter à ce fichier et envoyer cette modification à votre +gestionnaire de source. Ensuite, à chaque fois qu'un développeur met à jour le projet +ou le déploie sur un serveur, Symfony vérifiera s'il y a des différences entre le fichier +standard ``parameters.yml.dist`` et votre fichier local ``parameters.yml``. S'il existe +une différence, Symfony vous demandera d'indiquer une valeur pour le nouveau paramètre +et l'ajoutera à votre fichier local ``parameters.yml``. + +Configuration liée à l'application +---------------------------------- + +.. best-practice:: + + Définissez les options de configuration liées au comportement de + l'application dans le fichier ``app/config/config.yml``. + +Le fichier ``config.yml`` contient les options utilisées par l'application pour +modifier son comportement, comme l'expéditeur des notifications par email ou +l'activation de `fonctionnalitées conditionnelles`_. Définir ces valeurs +dans le fichier ``parameters.yml`` serait ajouter une couche supplémentaire de +configuration qui ne serait pas nécessaire car vous ne voulez pas ou n'avez pas +besoin de modifier ces valeurs de configuration sur chaque serveur. + +Les options de configuration définies dans le fichier ``config.yml`` varient +généralement d'un `environnement d'exécution`_ à l'autre. C'est pourquoi Symfony +inclut déjà les fichiers ``app/config/config_dev.yml`` et ``app/config/config_prod.yml`` +de sorte que vous puissiez indiquer des valeurs spécifiques à chaque environnement. + +Constantes vs Options de Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +L'une des erreurs les plus commune lors de la définition de la configuration +de l'application est de créer de nouvelles options pour les valeurs qui ne +change jamais, comme le nombre d'éléments pour des résultats paginés. + +.. best-practice:: + + Utilisez des constantes pour définir les options de configuration ne changeant que rarement. + +L'approche traditionnelle pour définir les options de configuration a impliqué +que beaucoup d'application Symfony ont inclut une option comme ce qui suit, qui +serait utilisée pour gérer le nombre de messages à afficher sur la page d'accueil +d'un blog : + +.. code-block:: yaml + + # app/config/config.yml + parameters: + homepage.num_items: 10 + +Si vous vous demandez quand est-ce la dernière fois que vous avez changé la valeur +d'une option de ce type, il y a de fortes chances pour que ce soit *jamais*. Créer une +option de configuration pour une valeur que vous ne configurerez jamais n'est pas +nécessaire. Notre recommandation est de définir ces valeurs en tant que constantes +dans votre application. Vous pourriez, par exemple, définir une constante ``NUM_ITEMS`` +dans l'entité ``Post`` : + +.. code-block:: php + + // src/AppBundle/Entity/Post.php + namespace AppBundle\Entity; + + class Post + { + const NUM_ITEMS = 10; + + // ... + } + +Le principal avantage à définir des constantes est que vous pouvez utiliser leur +valeur partout dans votre application. Lorsque vous utilisez des paramètres, ils +ne sont disponibles que lorsque vous avez accès au container Symfony. + +Les constantes peuvent être utilisées par exemple dans vos templates Twig grâce +à la fonction ``constant()`` : + +.. code-block:: html+jinja + +

+ Displaying the {{ constant('NUM_ITEMS', post) }} most recent results. +

+ +Et les entités et repositories Doctrine peuvent accéder facilement à ces valeurs, +alors qu'elles n'ont pas accès aux paramètres du container : + +.. code-block:: php + + namespace AppBundle\Repository; + + use Doctrine\ORM\EntityRepository; + use AppBundle\Entity\Post; + + class PostRepository extends EntityRepository + { + public function findLatest($limit = Post::NUM_ITEMS) + { + // ... + } + } + +Le seul inconvénient notable de l'utilisation des constantes pour ce type de +configuration est que vous ne pouvez pas les redéfinir facilement dans vos tests. + +Configuration Sémantique: Ne le faites pas +------------------------------------------ + +.. best-practice:: + + Ne définissez pas une configuration sémantique d'injection de dépendance pour vos bundle. + +Comme expliqué dans l'article `Comment exposer une configuration sémantique pour un Bundle`_, +les bundles Symfony ont deux possibilités concernant la gestion de la configuration : la +configuration normale des serveur via le fichier ``services.yml`` et la configuration +sémantique via une classe spécifique ``*Extension``. + +Bien que la configuration sémantique soit beaucoup plus puissante et fournisse des +fonctionnalités intéressante comme la validation, la charge de travail nécessaire +pour définir cette configution n'est pas valable pour vos bundles qui ne sont pas +destinés à être partagés en tant que bundle réutilisable. + +Déplacez les options sensibles entièrement en dehors de Symfony +--------------------------------------------------------------- + +Lorsque vous manipulez des options sensibles, comme des accès à une base de données, nous +vous recommendons de les stocker en dehors du projet Symfony et de les rendre disponible +au travers des variables d'environnement. Apprenez comme faire en suivant cet article : +`Comment configurer les paramètres externes dans le conteneur de services`_ + +.. _`fonctionnalitées conditionnelles`: http://en.wikipedia.org/wiki/Feature_toggle +.. _`environnement d'exécution`: http://symfony.com/doc/current/cookbook/configuration/environments.html +.. _`constant() function`: http://twig.sensiolabs.org/doc/functions/constant.html +.. _`Comment exposer une configuration sémantique pour un Bundle`: http://symfony.com/fr/doc/current/cookbook/bundles/extension.html +.. _`Comment configurer les paramètres externes dans le conteneur de services`: http://symfony.com/fr/doc/current/cookbook/configuration/external_parameters.html diff --git a/best_practices/controllers.rst b/best_practices/controllers.rst new file mode 100644 index 000000000..05cd7a1af --- /dev/null +++ b/best_practices/controllers.rst @@ -0,0 +1,212 @@ +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. + +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. + +.. best-practice:: + + Make your controller extend the ``FrameworkBundle`` base Controller 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. + +Routing Configuration +--------------------- + +To load routes defined as annotations in your controllers, add the following +configuration to the main routing configuration file: + +.. code-block:: yaml + + # app/config/routing.yml + app: + resource: "@AppBundle/Controller/" + type: annotation + +This configuration will load annotations from any controller stored inside the +``src/AppBundle/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/ + └─ AppBundle/ + ├─ ... + └─ 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. For +that reason, we don't recommend 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 makes +it less obvious to beginners that a controller should always return a Response +object (unless you're using a view layer). + +Lastly, the ``@Template`` annotation uses a ``TemplateListener`` class that hooks +into the ``kernel.view`` event dispatched by the framework. This listener introduces +a measurable performance impact. In the sample blog application, rendering the +homepage took 5 milliseconds using the ``$this->render()`` method and 26 milliseconds +using the ``@Template`` annotation. + +How the Controller Looks +------------------------ + +Considering all this, here is an example of how the controller should look +for the homepage of our app: + +.. code-block:: php + + namespace AppBundle\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; + + class DefaultController extends Controller + { + /** + * @Route("/", name="homepage") + */ + public function indexAction() + { + $em = $this->getDoctrine()->getManager(); + $posts = $em->getRepository('App:Post')->findLatest(); + + return $this->render('default/index.html.twig', array( + 'posts' => $posts + )); + } + } + +.. _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: + +.. code-block:: php + + /** + * @Route("/{id}", name="admin_post_show") + */ + public function showAction(Post $post) + { + $deleteForm = $this->createDeleteForm($post); + + return $this->render('admin/post/show.html.twig', array( + '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. + +When Things Get More Advanced +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This 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) + { + $post = $this->getDoctrine() + ->getRepository('AppBundle:Post') + ->findOneBy(array('slug' => $postSlug)); + + if (!$post) { + throw $this->createNotFoundException(); + } + + // ... + } + +You can also use the ``@ParamConverter`` configuration, which is infinitely +flexible: + +.. code-block:: php + + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; + + /** + * @Route("/comment/{postSlug}/new", name = "comment_new") + * @ParamConverter("post", options={"mapping": {"postSlug": "slug"}}) + */ + public function newAction(Request $request, Post $post) + { + // ... + } + +The point is this: the ParamConverter shortcut is great for simple situations. +But you shouldn't forget that querying for entities directly is still very +easy. + +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 `set up before/after filters`_. + +.. _`ParamConverter`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html +.. _`set up before/after filters`: http://symfony.com/doc/current/cookbook/event_dispatcher/before_after_filters.html diff --git a/best_practices/creating-the-project.rst b/best_practices/creating-the-project.rst new file mode 100644 index 000000000..f14190c2e --- /dev/null +++ b/best_practices/creating-the-project.rst @@ -0,0 +1,255 @@ +Créer le projet +=============== + +Installer Symfony +----------------- + +Il n'y a qu'une seule voie recommandée pour installer Symfony : + +.. best-practice:: + + Utilisez toujours `Composer`_ pour installer Symfony. + +Composer est le gestionnaire de dépendances utilisé par les applications PHP modernes. +Ajoutez ou supprimez des prérequis à votre projet et mettez à jour les bibliothèques +tierces utilisées par votre code est un vrai bonheur avec Composer. + +Gestion des dépendances avec Composer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Avant d'installer Symfony, vous devez être sûr que vous avez Composer d'installé +globalement. Ouvrez votre terminal (aussi appelé *console de commandes*) et exécutez +la commande suivante : + +.. code-block:: bash + + $ composer --version + Composer version 1e27ff5e22df81e3cd0cd36e5fdd4a3c5a031f4a 2014-08-11 15:46:48 + +Vous verrez probablement un identifiant de version différent. Ce n'est pas grave +car Composer est mis à jour de manière continuelle et sa version spécifique n'a +pas d'importance. + + +Installer Composer globalement +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Dans le cas où vous n'auriez pas Composer d'installer globalement, exécutez les +deux commandes suivantes si vous utilisez Linux ou Max OS X (la seconde commande +vous demandera votre mot de passe utilisateur) : + +.. code-block:: bash + + $ curl -sS https://getcomposer.org/installer | php + $ sudo mv composer.phar /usr/local/bin/composer + +.. note:: + + En fonction de votre distribution Linux, vous devrez exécuter la commande ``su`` + au lieu de ``sudo``. + +Si vous utilisez un système Windows, téléchargez l'installateur depuis la +`page de téléchargement de Composer`_ et suivez les étapes pour l'installer. + +Créer l'application de blog +--------------------------- + +Maintenant que tout est correctement paramétré, vous pouvez créer un nouveau +projet basé sur Symfony. Dans votre console, allez dans un répertoire où vous +avez le droit de créer des fichiers et exécutez les commandes suivantes : + +.. code-block:: bash + + $ cd projects/ + $ composer create-project symfony/framework-standard-edition blog/ + +Cette commande créera un nouveau répertoire appelé ``blog`` qui contiendra +un nouveau projet basé sur la version stable la plus récente de Symfony disponible. + +Vérifier l'installation de Symfony +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Une fois l'installation terminée, allez dans le répertoire ``blog/`` et vérifiez +que Symfony est correctement installé en exécutant la commande suivante : + +.. code-block:: bash + + $ cd blog/ + $ php app/console --version + + Symfony version 2.6.* - app/dev/debug + +Si vous voyez la version de Symfony installée, tout fonctionne comme attendu. Sinon, +vous pouvez exécuter le *script* suivant pour vérifier ce qui empêche votre système +d'exécuter correctement des applications Symfony : + +.. code-block:: bash + + $ php app/check.php + +En fonction de votre système, vous pouvez voir jusqu'à deux listes différentes +lors de l'exécution du script `check.php`. La première montre les prérequis +obligatoires que votre système doit avoir pour pouvoir exécuter des applications +Symfony. La seconde liste montre les prérequis facultatifs suggérés pour une +exécution optimal des applications Symfony : + +.. code-block:: bash + + Symfony2 Requirements Checker + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + > PHP is using the following php.ini file: + /usr/local/zend/etc/php.ini + + > Checking Symfony requirements: + .....E.........................W..... + + [ERROR] + Your system is not ready to run Symfony2 projects + + Fix the following mandatory requirements + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + * date.timezone setting must be set + > Set the "date.timezone" setting in php.ini* (like Europe/Paris). + + Optional recommendations to improve your setup + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + * short_open_tag should be disabled in php.ini + > Set short_open_tag to off in php.ini*. + + +.. tip:: + + Les distributions de Symfony sont signées numériquement pour des raisons de sécurité. + Si vous souhaitez vérifier l'intégrité de votre installation Symfony, regardez le + `dépôt public des sommes de contrôle`_ et suivez `ces étapes`_ pour vérifier les signatures. + +Structurer l'application +------------------------ + +Après avoir créé l'application, allez dans le répertoire ``blog/`` et vous verrez un +certain nombre de fichiers et répertoires générés automatiquement : + +.. code-block:: text + + blog/ + ├─ app/ + │ ├─ console + │ ├─ cache/ + │ ├─ config/ + │ ├─ logs/ + │ └─ Resources/ + ├─ src/ + │ └─ AppBundle/ + ├─ vendor/ + └─ web/ + +Cette architecture de fichers et de répertoires est une convention proposée par +Symfony pour la structure de vos application. L'usage recommandé pour chaque +répertoire est le suivant : + +* ``app/cache/``, stocke tous les fichiers de cache générés par l'application; +* ``app/config/``, stocke toute la configuration définie pour chaque environnement; +* ``app/logs/``, stocke tous les fichiers de journaux (logs) générés par l'application; +* ``app/Resources/``, stocke tous les fichiers de templates et de traduction pour l'application; +* ``src/AppBundle/``, stocke tout le code Symfony spécifique (contrôleurs et routes), + votre code métier (ex: classes Doctrine) et toute votre logique métier; +* ``vendor/``, c'est le répertoire où Composer installe les dépendances de votre application + et vous ne devez jamais modifier son contenu; +* ``web/``, stocke tous les fichiers des contrôleurs frontaux et toutes les ressources web, telles que + les feuilles de style, les fichiers JavaScript et les images. + +Application Bundles +~~~~~~~~~~~~~~~~~~~ + +Quand Symfony 2.0 est sorti, beaucoup de développeurs ont naturellement adopté +la voie de symfony 1.x en divisant leurs applications en modules logiques. C'est +pourquoi beaucoup d'applications Symfony utilisent les bundles pour diviser leur +code en fonctionnalités logiques : ``UserBundle``, ``ProductBundle``, ``InvoiceBundle``, +etc. + +Mais un bundle *entend* être quelque chose pouvant être réutilisé comme un élément +de logiciel à part. Si ``UserBundle`` ne peut pas être réutilisé *"en l'état"* dans +une autre application Symfony, alors il ne devrait pas être son propre bundle. Tout +comme si ``InvoiceBundle`` dépend de ``ProductBundle``, alors il n'y a pas d'avantage +à avoir deux bundles spérarés. + +.. best-practice:: + + Créez seulement un bundle appelé ``AppBundle`` pour votre application métier + +Implémenter un simple bundle ``AppBundle`` dans vos projet rendra votre code plus +concis et plus simple à comprendre. À partir de Symfony 2.6, la documentation +officielle de Symfony utilisera le nom ``AppBundle``. + +.. note:: + + Il n'est pas nécessaire de préfixer le ``AppBundle`` avec votre propre nom d'organisation + (ex: ``AcmeAppBundle``), car ce bundle applicatif n'a aucune vocation à être partagé. + +Au final, ceci est la structure typique d'une application Symfony suivant ces bonnes +pratiques : + +.. code-block:: text + + blog/ + ├─ app/ + │ ├─ console + │ ├─ cache/ + │ ├─ config/ + │ ├─ logs/ + │ └─ Resources/ + ├─ src/ + │ └─ AppBundle/ + ├─ vendor/ + └─ web/ + ├─ app.php + └─ app_dev.php + +.. tip:: + + Si vous utilisez Symfony 2.6 ou une version plus récente, le bundle ``AppBundle`` + est déjà généré pour vous. Si vous utilisez une ancienne version de Symfony, vous + pouvez le générer à la main en exécutant cette commande : + + .. code-block:: bash + + $ php app/console generate:bundle --namespace=AppBundle --dir=src --format=annotation --no-interaction + +Étendre la structure des répertoires +------------------------------------ + +Si vos projets ou votre infrastructure requiert quelques changement dans les +répertoires par défaut de la structure de Symfony, vous pouvez +`surcharger l'emplacement des répertoires principaux`_ : +``cache/``, ``logs/`` and ``web/``. + +En plus, Symfony3 utilisera une structure de répertoire légèrement différentes +lorsqu'il sortira : + +.. code-block:: text + + blog-symfony3/ + ├─ app/ + │ ├─ config/ + │ └─ Resources/ + ├─ bin/ + │ └─ console + ├─ src/ + ├─ var/ + │ ├─ cache/ + │ └─ logs/ + ├─ vendor/ + └─ web/ + +Les changements sont vraiment superficiels, mais pour le moment, nous vous +recommandons d'utiliser la structure de répertoire de Symfony2. + +.. _`Composer`: https://getcomposer.org/ +.. _`Get Started`: https://getcomposer.org/doc/00-intro.md +.. _`page de téléchargement de Composer`: https://getcomposer.org/download/ +.. _`surcharger l'emplacement des répertoires principaux`: http://symfony.com/doc/current/cookbook/configuration/override_dir_structure.html +.. _`dépôt public des sommes de contrôle`: https://github.com/sensiolabs/checksums +.. _`ces étapes`: http://fabien.potencier.org/article/73/signing-project-releases diff --git a/best_practices/forms.rst b/best_practices/forms.rst new file mode 100644 index 000000000..6d70561e9 --- /dev/null +++ b/best_practices/forms.rst @@ -0,0 +1,231 @@ +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. Honestly, unless you need to reuse the form somewhere else, that's +totally fine. But for organize and reuse, we recommend that you define each +form in its own PHP class: + +.. code-block:: php + + namespace AppBundle\Form; + + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolverInterface; + + class PostType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('title') + ->add('summary', 'textarea') + ->add('content', 'textarea') + ->add('authorEmail', 'email') + ->add('publishedAt', 'datetime') + ; + } + + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'data_class' => 'AppBundle\Entity\Post' + )); + } + + public function getName() + { + return 'post'; + } + } + +To use the class, use ``createForm`` and instantiate the new class: + +.. code-block:: php + + use AppBundle\Form\PostType; + // ... + + public function newAction(Request $request) + { + $post = new Post(); + $form = $this->createForm(new PostType(), $post); + + // ... + } + +Registering Forms as Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also `register your form type as a service`_. But this is *not* recommended +unless you plan to reuse the new form type in many places or embed it in +other forms directly or via the `collection type`_. + +For most forms that are used only to edit or create something, registering +the form as a service is over-kill, and makes it more difficult to figure +out exactly which form class is being used in a controller. + +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. + +Since Symfony 2.5, you can 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: + +.. code-block:: php + + class PostType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('save', 'submit', array('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: + +.. code-block:: php + + namespace AppBundle\Controller\Admin; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use AppBundle\Entity\Post; + use AppBundle\Form\PostType; + + class PostController extends Controller + { + // ... + + public function newAction(Request $request) + { + $post = new Post(); + $form = $this->createForm(new PostType(), $post); + $form->add('submit', 'submit', array( + 'label' => 'Create', + 'attr' => array('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+jinja + +
+ {{ form_widget(form) }} + + +
+ +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. + +The simplest way - which is especially useful during development - is to render +the form tags manually and then use ``form_widget()`` to render all of the fields: + +.. code-block:: html+jinja + +
+ {{ form_widget(form) }} +
+ +.. best-practice:: + + Don't use the ``form()`` or ``form_start()`` functions to render the + starting and ending form tags. + +Experienced Symfony developers will recognize that we're rendering the ``
`` +tags manually instead of using the ``form_start()`` or ``form()`` functions. +While those are convenient, they take away from some clarity with little +benefit. + +.. tip:: + + The exception is a delete form because it's really just one button and + so benefits from some of these extra shortcuts. + +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 `How to Customize Form Rendering`_ 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: + +.. code-block:: php + + public function newAction(Request $request) + { + // build the form ... + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $em = $this->getDoctrine()->getManager(); + $em->persist($post); + $em->flush(); + + return $this->redirect($this->generateUrl( + 'admin_post_show', + array('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, we recommend using ``$form->isSubmitted()`` in the ``if`` statement +for clarity. This isn't technically needed, since ``isValid()`` first calls +``isSubmitted()``. But without this, the flow doesn't read well as it *looks* +like the form is *always* processed (even on the GET request). + +.. _`register your form type as a service`: http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html#creating-your-field-type-as-a-service +.. _`collection type`: http://symfony.com/doc/current/reference/forms/types/collection.html +.. _`How to Customize Form Rendering`: http://symfony.com/doc/current/cookbook/form/form_customization.html +.. _`form event system`: http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html diff --git a/best_practices/i18n.rst b/best_practices/i18n.rst new file mode 100644 index 000000000..1e7a86f31 --- /dev/null +++ b/best_practices/i18n.rst @@ -0,0 +1,96 @@ +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: + +.. code-block:: yaml + + # app/config/config.yml + framework: + # ... + translator: { fallback: "%locale%" } + + # app/config/parameters.yml + parameters: + # ... + locale: en + +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 2.6 added support for notes inside 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 Apache-licensed `JMSTranslationBundle`_ offers you a web interface for + 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 lets you override translations defined in third-party bundles. + +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 + + + + + +.. _`JMSTranslationBundle`: https://github.com/schmittjoh/JMSTranslationBundle diff --git a/best_practices/index.rst b/best_practices/index.rst new file mode 100644 index 000000000..e6e712f58 --- /dev/null +++ b/best_practices/index.rst @@ -0,0 +1,16 @@ +Official Symfony Best Practices +=============================== + +.. toctree:: + + introduction + creating-the-project + configuration + business-logic + controllers + templates + forms + i18n + security + web-assets + tests diff --git a/best_practices/introduction.rst b/best_practices/introduction.rst new file mode 100644 index 000000000..f5d4ef5db --- /dev/null +++ b/best_practices/introduction.rst @@ -0,0 +1,107 @@ +.. index:: + single: Les bonnes pratiques du framework Symfony + +Les bonnes pratiques du framework Symfony +========================================= + +Le framework Symfony est bien connu pour être *réellement* flexible et utilisé +pour construire des micro-site, des applications d'entreprise permettant de tenir +des milliard de connexions et être la base d'autres frameworks. Depuis qu'il est +sorti en juillet 2011, la communauté a appris énormément sur ce qui est possible +et comment faire les choses *de la meilleure manière*. + +Les ressources de la communauté - comme les blogs ou les présentations - ont créées +un ensemble de recommandations non-officielles pour développer des applications +Symfony. Malheureusement, un certain nombre de ces recommandations ne sont pas +nécessaires pour les applications web. La plupart du temps, elles compliquent +inutilement les choses et ne suivent pas la philosophie pragmatique de Symfony. + +Qu'apporte ce guide ? +--------------------- + +Ce guide entends définir ceci en décrivant les **bonnes pratiques officielles +permettant de développer des applications web avec le framework Symfony**. Elles +sont les bonnes pratiques qui collent à la philosophie du framework telle +qu'imaginée par le créateur original `Fabien Potencier`_. + +.. note:: + + **Bonnes pratiques** est une expression désignant *"un ensemble de procédures + définies qui permettent de produire des résultats optimums"*. Et c'est exactement + ce que ce guide entend procurer. Sauf si vous n'êtes pas d'accord avec + l'ensemble des recommandations, nous pensons qu'elles peuvent vous aider + à construire de grosses application avec moins de complexité. + +Ce guide est *particulièrement adapté* pour : + +* Les sites internet et les applications web développés avec le framework Symfony. + +Pour les autres cas, ce guide devrait être un bon **point de départ** que vous +pouvez ensuite **amender et adapter à vos besoins**: + +* Bundles partagés avec la communauté Symfony; +* Développeurs avancés ou équipe créant leurs propres standards; +* Quelques applications complexe ayant des pré-requis fortement personnalisés; +* Bundles devant être partagés en interne dans une entreprise. + +Nous savons que les vieilles habitudes ont la vie dure et que certains d'entre +vous seront choqués par certaines de ces bonnes pratiques. Mais en suivant +celles-ci vous serez capable de développer des applications rapides, en +retirant de la complexité et avec le même niveau ou plus de qualité. C'est +également un objet mouvant qui continuera à s'améliorer. + +Gardez en tête que ce sont des **recommandations facultatives** que vous +et votre équipe pouvait ou ne pouvait pas suivre pour développer des +applications Symfony. Si vous souhaitez continuer à suivre vos propres +bonnes pratiques et méthodologies, vous pouvez bien entendu faire cela. +Symfony est assez flexible pour s'adapter à vos besoins. Cela ne changera +jamais. + +À qui s'adresse ce livre (Indice : ce n'est pas un tutoriel) +------------------------------------------------------------ + +Chaque développeur Symfony, que vous soyez expert ou novice, peut lire ce +guide. Mais, comme ce n'est pas un tutoriel, quelques connaissances de base +sur Symfony sont requises pour pouvoir tout comprendre. Si vous êtes totalement +novice avec Symfony, bienvenu ! Commencez par le premier tutoriel `The Quick Tour`_. + +Nous avons volontairement garder ce guide court. Nous ne voulons par répéter des +explications que vous pouvez trouver dans la vaste documentation de Symfony, +comme les discussions autour de l'injection de dépendance ou des contrôleur frontaux. +Nous allons uniquement mettre l'accent sur l'explication de la façon de faire de +ce que vous connaissez déjà. + +L'application +------------- + +En complément de ce guide, vous trouverez un exemple d'application développée +avec à l'esprit l'ensemble des bonnes pratiques. **L'application est un simple +moteur de blog**, car cela permet de ne se concentrer que sur les concepts et +fonctionnalités de Symfony sans s'embarrasser de détails complexes. + +Au lieu de développer l'application étape par étape dans ce guide, vous trouverez +une sélection d'extrait de code à travers les chapitres. Veuillez vous référer +au dernier chapitre de ce guide pour trouver plus de détails au sujet de +l'application et des instructions pour l'installer. + +Ne mettez pas à jour vos applications existantes +------------------------------------------------ + +Après avoir lu ce manuel, certains d'entre vous pourrait vouloir refactoriser +leurs applications Symfony existantes. Notre recommandation est simple et +claire : **vous ne devriez pas refactoriser vos applications existante pour +suivre ces bonnes pratiques**. Les raisons de ne pas le faire sont nombreuses : + +* Vos applications existantes ne sont pas mauvaises, elles suivent simplement + un ensemble d'autres lignes directrices; +* Une refactorisation complète d'une base de code présente un risque d'introduire + des erreurs dans vos applications; +* La somme de travail nécessaire à cela pourrait être mieux dépenser à + améliorer vos tests ou à ajouter des fonctionnalités procurant une réelle + plus-value à vos utilisateurs finaux. + +.. _`Fabien Potencier`: https://connect.sensiolabs.com/profile/fabpot +.. _`The Quick Tour`: http://symfony.com/doc/current/quick_tour/the_big_picture.html +.. _`The Official Symfony Book`: http://symfony.com/doc/current/book/index.html +.. _`The Symfony Cookbook`: http://symfony.com/doc/current/cookbook/index.html +.. _`github.com/.../...`: http://github.com/.../... diff --git a/best_practices/security.rst b/best_practices/security.rst new file mode 100644 index 000000000..026c672bc --- /dev/null +++ b/best_practices/security.rst @@ -0,0 +1,363 @@ +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 `Security Cookbook Section`_ has a lot of information about this. + +Regardless of your needs, authentication is configured in ``security.yml``, +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. There are exceptions +of course, especially if you have separated web and API sections on your +site. 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 ``bcrypt`` encoder for encoding your users' passwords. + +If your users have a password, then we recommend encoding 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 +remain resistant to brute-force search attacks. + +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 + + security: + encoders: + AppBundle\Entity\User: bcrypt + + providers: + database_users: + entity: { class: AppBundle:User, property: username } + + firewalls: + secured_area: + pattern: ^/ + anonymous: true + form_login: + check_path: security_login_check + login_path: security_login_form + + 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 `security.yml`_, the :ref:`@Security annotation ` +and using :ref:`isGranted ` on the ``security.context`` +service directly. + +.. best-practice:: + + * For protecting broad URL patterns, use ``access_control``; + * Whenever possible, use the ``@Security`` annotation; + * Check security directly on the ``security.context`` 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. + +.. 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. + +.. _best-practices-security-annotation: + +The @Security Annotation +------------------------ + +For controlling access on a controller-by-controller basis, use the ``@Security`` +annotation whenever possible. It's easy to read and is placed consistently +above each action. + +In our application, you need the ``ROLE_ADMIN`` in order to create a new post. +Using ``@Security``, this looks like: + +.. code-block:: php + + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; + // ... + + /** + * Displays a form to create a new Post entity. + * + * @Route("/new", name="admin_post_new") + * @Security("has_role('ROLE_ADMIN')") + */ + public function newAction() + { + // ... + } + +Using Expressions for Complex Security Restrictions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your security logic is a little bit more complex, you can use an `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: + +.. code-block:: php + + use AppBundle\Entity\Post; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; + + /** + * @Route("/{id}/edit", name="admin_post_edit") + * @Security("user.getEmail() == post.getAuthorEmail()") + */ + public function editAction(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 easily +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+jinja + + {% if app.user and app.user.email == post.authorEmail %} + ... + {% endif %} + +The easiest solution - if your logic is simple enough - is to add a new method +to the ``Post`` entity that checks if a given user is its author: + +.. code-block:: php + + // src/AppBundle/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: + +.. code-block:: php + + use AppBundle\Entity\Post; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; + + /** + * @Route("/{id}/edit", name="admin_post_edit") + * @Security("post.isAuthor(user)") + */ + public function editAction(Post $post) + { + // ... + } + +.. code-block:: html+jinja + + {% if post.isAuthor(app.user) %} + ... + {% endif %} + +.. _best-practices-directy-isGranted: + +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 a ``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: + +.. code-block:: php + + /** + * @Route("/{id}/edit", name="admin_post_edit") + */ + public function editAction($id) + { + $post = $this->getDoctrine()->getRepository('AppBundle:Post') + ->find($id); + + if (!$post) { + throw $this->createNotFoundException(); + } + + if (!$post->isAuthor($this->getUser())) { + 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 an order +of magnitude easier than `ACL's`_ 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: + +.. code-block:: php + + namespace AppBundle\Security; + + use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter; + use Symfony\Component\Security\Core\User\UserInterface; + + // AbstractVoter class requires Symfony 2.6 or higher version + class PostVoter extends AbstractVoter + { + const CREATE = 'create'; + const EDIT = 'edit'; + + protected function getSupportedAttributes() + { + return array(self::CREATE, self::EDIT); + } + + protected function getSupportedClasses() + { + return array('AppBundle\Entity\Post'); + } + + protected function isGranted($attribute, $post, $user = null) + { + if (!$user instanceof UserInterface) { + return false; + } + + if ($attribute === self::CREATE && in_array('ROLE_ADMIN', $user->getRoles(), true)) { + return true; + } + + if ($attribute === self::EDIT && $user->getEmail() === $post->getAuthorEmail()) { + return true; + } + + return false; + } + } + +To enable the security voter in the application, define a new service: + +.. code-block:: yaml + + # app/config/services.yml + services: + # ... + post_voter: + class: AppBundle\Security\PostVoter + public: false + tags: + - { name: security.voter } + +Now, you can use the voter with the ``@Security`` annotation: + +.. code-block:: php + + /** + * @Route("/{id}/edit", name="admin_post_edit") + * @Security("is_granted('edit', post)") + */ + public function editAction(Post $post) + { + // ... + } + +You can also use this directly with the ``security.context`` service, or +via the even easier shortcut in a controller: + +.. code-block:: php + + /** + * @Route("/{id}/edit", name="admin_post_edit") + */ + public function editAction($id) + { + $post = // query for the post ... + + if (!$this->get('security.context')->isGranted('edit', $post)) { + throw $this->createAccessDeniedException(); + } + } + +Learn More +---------- + +The `FOSUserBundle`_, developed by the Symfony community, adds support for a +database-backed user system in Symfony2. It also handles common tasks like +user registration and forgotten password functionality. + +Enable the `Remember Me feature`_ to allow your users to stay logged in for +a long period of time. + +When providing customer support, sometimes it's necessary to access the application +as some *other* user so that you can reproduce the problem. Symfony provides +the ability to `impersonate users`_. + +If your company uses a user login method not supported by Symfony, you can +develop `your own user provider`_ and `your own authentication provider`_. + +.. _`Security Cookbook Section`: http://symfony.com/doc/current/cookbook/security/index.html +.. _`security.yml`: http://symfony.com/doc/current/reference/configuration/security.html +.. _`ParamConverter`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html +.. _`@Security annotation`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html +.. _`security.yml`: http://symfony.com/doc/current/reference/configuration/security.html +.. _`security voter`: http://symfony.com/doc/current/cookbook/security/voters_data_permission.html +.. _`Acces Control List`: http://symfony.com/doc/current/cookbook/security/acl.html +.. _`ACL's`: http://symfony.com/doc/current/cookbook/security/acl.html +.. _`expression`: http://symfony.com/doc/current/components/expression_language/introduction.html +.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle +.. _`Remember Me feature`: http://symfony.com/doc/current/cookbook/security/remember_me.html +.. _`impersonate users`: http://symfony.com/doc/current/cookbook/security/impersonating_user.html +.. _`your own user provider`: http://symfony.com/doc/current/cookbook/security/custom_provider.html +.. _`your own authentication provider`: http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html diff --git a/best_practices/templates.rst b/best_practices/templates.rst new file mode 100644 index 000000000..a53b0a304 --- /dev/null +++ b/best_practices/templates.rst @@ -0,0 +1,164 @@ +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 much more verbose than in Twig 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). + +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 logical name +to refer to them (e.g. ``AcmeDemoBundle: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: + +================================================== ================================== +Templates stored inside bundles Templates stored in ``app/`` +================================================== ================================== +``AcmeDemoBunde:Default:index.html.twig`` ``default/index.html.twig`` +``::layout.html.twig`` ``layout.html.twig`` +``AcmeDemoBundle::index.html.twig`` ``index.html.twig`` +``AcmeDemoBundle:Default:subdir/index.html.twig`` ``default/subdir/index.html.twig`` +``AcmeDemoBundle: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. + +Twig Extensions +--------------- + +.. best-practice:: + + Define your Twig extensions in the ``AppBundle/Twig/`` directory and + configure them using the ``app/config/services.yml`` file. + +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:: bash + + $ composer require erusev/parsedown + +Then, create a new ``Markdown`` service that will be used later by the Twig +extension. The service definition only requires the path to the class: + +.. code-block:: yaml + + # app/config/services.yml + services: + # ... + markdown: + class: AppBundle\Utils\Markdown + +And the ``Markdown`` class just needs to define one single method to transform +Markdown content into HTML:: + + namespace AppBundle\Utils; + + class Markdown + { + private $parser; + + public function __construct() + { + $this->parser = new \Parsedown(); + } + + public function toHtml($text) + { + $html = $this->parser->text($text); + + return $html; + } + } + +Next, create a new Twig extension and define a new filter called ``md2html`` +using the ``Twig_SimpleFilter`` class. Inject the newly defined ``markdown`` +service in the constructor of the Twig extension: + +.. code-block:: php + + namespace AppBundle\Twig; + + use AppBundle\Utils\Markdown; + + class AppExtension extends \Twig_Extension + { + private $parser; + + public function __construct(Markdown $parser) + { + $this->parser = $parser; + } + + public function getFilters() + { + return array( + new \Twig_SimpleFilter( + 'md2html', + array($this, 'markdownToHtml'), + array('is_safe' => array('html')) + ), + ); + } + + public function markdownToHtml($content) + { + return $this->parser->toHtml($content); + } + + public function getName() + { + return 'app_extension'; + } + } + +Lastly define a new service to enable this Twig extension in the app (the service +name is irrelevant because you never use it in your own code): + +.. code-block:: yaml + + # app/config/services.yml + services: + app.twig.app_extension: + class: AppBundle\Twig\AppExtension + arguments: ["@markdown"] + tags: + - { name: twig.extension } + + +.. _`Twig`: http://twig.sensiolabs.org/ +.. _`Parsedown`: http://parsedown.org/ +.. _`Twig global variables`: http://symfony.com/doc/master/cookbook/templating/global_variables.html +.. _`override error pages`: http://symfony.com/doc/current/cookbook/controller/error_pages.html +.. _`render a template without using a controller`: http://symfony.com/doc/current/cookbook/templating/render_without_controller.html diff --git a/best_practices/tests.rst b/best_practices/tests.rst new file mode 100644 index 000000000..0bbcbd665 --- /dev/null +++ b/best_practices/tests.rst @@ -0,0 +1,114 @@ +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. + +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. + +A functional test can be as easy as this: + +.. code-block:: php + + /** @dataProvider provideUrls */ + public function testPageIsSuccessful($url) + { + $client = self::createClient(); + $client->request('GET', $url); + + $this->assertTrue($client->getResponse()->isSuccessful()); + } + + public function provideUrls() + { + return array( + array('/'), + array('/posts'), + array('/post/fixture-post-1'), + array('/blog/category/fixture-category'), + array('/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: + +.. code-block:: php + + public function testBlogArchives() + { + $client = self::createClient(); + $url = $client->getContainer()->get('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. + +Of course, if you have a heavy JavaScript frontend, you should consider using +pure JavaScript-based testing tools. + +Learn More about Functional Tests +--------------------------------- + +Consider using `Faker`_ and `Alice`_ libraries to generate real-looking data +for your test fixtures. + +.. _`Faker`: https://github.com/fzaninotto/Faker +.. _`Alice`: https://github.com/nelmio/alice +.. _`PhpUnit`: https://phpunit.de/ +.. _`PhpSpec`: http://www.phpspec.net/ +.. _`Mink`: http://mink.behat.org +.. _`smoke testing`: http://en.wikipedia.org/wiki/Smoke_testing_(software) diff --git a/best_practices/web-assets.rst b/best_practices/web-assets.rst new file mode 100644 index 000000000..e77e3db77 --- /dev/null +++ b/best_practices/web-assets.rst @@ -0,0 +1,97 @@ +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. + +.. best-practice:: + + Store your assets in the ``web/`` directory. + +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+jinja + + + + + {# ... #} + + + + +.. note:: + + Keep in mind that ``web/`` is a public directory and that anything stored + here will be publicly accessible. For that reason, you should put your + compiled web assets here, but not their source files (e.g. SASS files). + +Using Assetic +------------- + +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. + +.. best-practice:: + + Use Assetic to compile, combine and minimize web assets, unless you're + comfortable with frontend tools like GruntJS. + +`Assetic`_ is an asset manager capable of compiling assets developed with +a lot of different frontend technologies like LESS, Sass and CoffeScript. +Combining all your assets with Assetic is a matter of wrapping all the assets +with a single Twig tag: + +.. code-block:: html+jinja + + {% stylesheets + 'css/bootstrap.min.css' + 'css/main.css' + filter='cssrewrite' output='css/compiled/all.css' %} + + {% endstylesheets %} + + {# ... #} + + {% javascripts + 'js/jquery.min.js' + 'js/bootstrap.min.js' + output='js/compiled/all.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). + +Learn More about Assetic +------------------------ + +Assetic can also minimize CSS and JavaScript assets `using UglifyCSS/UglifyJS`_ +to speed up your websites. You can even `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 features. + +.. _`Assetic`: http://symfony.com/doc/current/cookbook/assetic/asset_management.html +.. _`using UglifyCSS/UglifyJS`: http://symfony.com/doc/current/cookbook/assetic/uglifyjs.html +.. _`compress images`: http://symfony.com/doc/current/cookbook/assetic/jpeg_optimize.html +.. _`official Assetic documentation`: https://github.com/kriswallsmith/assetic diff --git a/conf.py b/conf.py index 9cd282cd9..fc47c272a 100644 --- a/conf.py +++ b/conf.py @@ -32,7 +32,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', - 'sensio.sphinx.refinclude', 'sensio.sphinx.configurationblock', 'sensio.sphinx.phpcode'] + 'sensio.sphinx.refinclude', 'sensio.sphinx.configurationblock', 'sensio.sphinx.phpcode', 'sensio.sphinx.bestpractice'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/index.rst b/index.rst index c96005fa0..8c750e2f8 100644 --- a/index.rst +++ b/index.rst @@ -38,6 +38,16 @@ Cookbook Lisez le :doc:`Cookbook`. +Bonnes pratiques +---------------- + +.. toctree:: + :hidden: + + best_practices/index + +Lisez le :doc:`Official Best Practices `. + Composants ----------