Introduction à Symfony2
• Hugo HAMON (@hhamon)
• Responsable des formations Sensio Labs • Secrétaire Général de l’AFUP
• 10 ans de développement web dont 8 avec PHP • Coauteur d’ouvrages Eyrolles • Apprendre-PHP.com / HugoHamon.com
Qu’est-ce que Symfony2 ?
Un framework web
PHP 5.3
Objectifs ?
• Développer plus vite et mieux • Faciliter le travail en équipe • Pérenniser les applications • Simpli!er la maintenance et les évolutions • Se concentrer sur la logique métier
• Ne pas réinventer la roue !
Symfony2 intègre les meilleurs outils Open-Source PHP
Symfony Components
Dependency Injection Container Request Handler Event Dispatcher
Console YAML
…
Zend Framework PHPUnit
Doctrine2 Swift Mailer
Twig
Différences avec symfony 1.x ?
Même philosophie, Même outillage,
Moins de concepts, Plus de #exibilité
Performances accrues
Où en est-on aujourd’hui ?
• Version ALPHA
• Briques logicielles manquantes
• Documentation incomplète
• L’API peut encore beaucoup évoluer
• Version stable repoussée à début Mars 2011
Je veux tester Symfony2 ! git clone http://github.com/symfony/symfony-sandbox.git
http://www.symfony-reloaded.org
Je veux développer un projet client maintenant avec Symfony2 ?
A ta place, je ne ferai pas ça…
Quel outillage ?
• Sécurité • Architecture MVC • URLs élégantes • DBAL & ORM • Outils de débogage • Formulaires • Con!guration
• Extensibilité • I18N & L10N • Authenti!cation et ACLs • Tests unitaires • Tests fonctionnels • Cache • Admin Generator
Architecture d’un projet Symfony2
Un Projet Symfony2 est un répertoire qui se compose d’une Application, d’un
jeu de Bundles et de librairies.
. |-- LICENSE |-- README |-- app/ | |-- AppCache.php | |-- AppKernel.php | |-- cache/ | |-- config/ | |-- console | |-- logs/ | |-- phpunit.xml.dist | `-- views/ |-- bin/ | |-- create_sandbox.sh | |-- install_vendors.sh | |-- prepare_vendors.sh | `-- update_vendors.sh |-- src/ | |-- Application/ | |-- Bundle/ | |-- autoload.php | `-- vendor/ `-- web/ |-- bundles/ |-- check.php |-- index.php `-- index_dev.php
Répertoire de l’Applica0on
Code de l’Applica0on + Bundles +
Librairies externes
Dossier public
Une Application est un répertoire qui contient la con!guration pour un jeu de
Bundles donné.
app/ |-- AppCache.php |-- AppKernel.php |-- cache/ |-- config/ | |-- config.yml | |-- config_dev.yml | |-- config_test.yml | |-- routing.yml | `-- routing_dev.yml |-- console |-- logs/ | `-- dev.log |-- phpunit.xml.dist `-- views/ |-- layout.php
The AppKernel class is the main class of the applica0on
Configura0on files
Logs and applica0on templates
Structure d’une application
Un Bundle est un ensemble structuré et cohérent de !chiers qui implémentent une fonctionnalité
(un blog, un forum, …) et qui peut facilement être partagé avec d’autres développeurs.
symfony 1.x => Plugins Symfony 2.x => Bundles
+ BlogBundle/ |-- Controller/ | `-- BlogController.php |-- Entity/ | `-- Post.php |-- Form/ | `-- PostForm.php |-- BlogBundle.php |-- Model/ | `-- PostRepository.php |-- Resources/ | |-- config/ | | `-- routing.yml | |-- views/ | | `-- Blog/ | | |-- showPost.php | | `-- listPost.php | `-- public/ | `-- css/ | `-- blog.css `-- Tests/ `-- Controller/ `-- BlogControllerTest.php
Le fichier BlogBundle.php est obligatoire et con0ent la classe qui
déclare le bundle.
Un bundle peut contenir de la configura0on, des templates et des
ressources web.
Un bundle peut aussi contenir des scripts de tests PHPUnit.
Code source du bundle : contrôleurs modèles, formulaires…
Sécurité
XSS CSRF
SQL Injections
Le dossier web/ du projet est le seul accessible depuis un navigateur web
Routage et URLs
Le système de routage a pour rôle de convertir une URL en une réponse web.
Elles sont propres et élégantes a!n d’exposer des informations pertinentes et de masquer
l’implémentation technique…
http://www.domain.com/blog/2010/09/15/symfony2-rocks
Con!guration des URLs en YAML # src/Bundle/BlogBundle/Resources/config/routing.yml
post_details: pattern: /blog/:year/:month/:day/:slug defaults: { _controller: BlogBundle:Blog:showPost }
Exemple d’URL générée http://www.domain.com/blog/2010/09/15/symfony2-rocks
Association URL et Code ?
# src/Application/HelloBundle/Resources/config/routing.yml
hello: pattern: /hello/:name defaults: { _controller: HelloBundle:Hello:index }
# src/Application/HelloBundle/Controller/HelloController.php
namespace Application\HelloBundle\Controller;
class HelloController extends Controller { public function indexAction($name) { // ... } }
# src/Application/HelloBundle/Resources/config/routing.yml
hello: pattern: /hello/:name defaults: { _controller: HelloBundle:Hello:index }
# src/Application/HelloBundle/Controller/HelloController.php
namespace Application\HelloBundle\Controller;
class HelloController extends Controller { public function indexAction($name) { // ... } }
Nom du Bundle
Un dossier / namespace
# src/Application/HelloBundle/Resources/config/routing.yml
hello: pattern: /hello/:name defaults: { _controller: HelloBundle:Hello:index }
# src/Application/HelloBundle/Controller/HelloController.php
namespace Application\HelloBundle\Controller;
class HelloController extends Controller { public function indexAction($name) { // ... } }
Nom du contrôleur
Une classe
# src/Application/HelloBundle/Resources/config/routing.yml
hello: pattern: /hello/:name defaults: { _controller: HelloBundle:Hello:index }
# src/Application/HelloBundle/Controller/HelloController.php
namespace Application\HelloBundle\Controller;
class HelloController extends Controller { public function indexAction($name) { // ... } }
Nom de l’ac0on
Une méthode
# src/Application/HelloBundle/Resources/config/routing.yml
hello: pattern: /hello/:name defaults: { _controller: HelloBundle:Hello:index }
# src/Application/HelloBundle/Controller/HelloController.php
namespace Application\HelloBundle\Controller;
class HelloController extends Controller { public function indexAction($name) { // ... } }
post_details: pattern: /blog/:year/:month/:day/:slug defaults: { _controller: BlogBundle:Blog:showPost }
namespace Application\BlogBundle\Controller;
class BlogController extends Controller { public function showPostAction($slug, $year) { // ... } }
Les paramètres peuvent être passés dans un ordre arbitraire
Architecture MVC
• Séparation du code en trois couches – Logique métier dans le Modèle – Logique applicative dans le Contrôleur – Affichage dans la Vue (templates)
• Modularité et découplage du code • Maintenance simpli!ée sur le code source • Code testable unitairement et plus robuste
Les actions pour la logique applicative.
Elles se situent dans les Contrôleurs.
# src/Application/BlogBundle/Resources/config/routing.yml
post_show: pattern: /blog/article/:id/show defaults: { _controller: BlogBundle:Blog:show }
๏ Une action est accessible depuis une URL
# src/Application/BlogBundle/Controller/BlogController.php
namespace Application\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller { public function showAction($id) { // find the article by its id $post = ...;
// render the view return $this->render('BlogBundle:Blog:show', array('post' => $post)); } }
Template à rendre Variables du template
Paramètres de l’url
Les templates constituent la couche de présentation des données, la vue.
# src/Application/BlogBundle/Resources/views/Blog/show.php
<?php $view->extend('::layout') ?>
<h2><?php echo $post->getTitle() ?></h2>
<p> <?php echo $post->getContent() ?> </p>
๏ Syntaxe alternative de PHP ๏ Quelques brèves instructions PHP (echo, if, foreach…) ๏ Echappement automatique des variables
Layout de décora0on
Variables échappées => pas de XSS !!!
Héritage de Vues
# src/Application/HelloBundle/Resources/views/Hello/index.php
<?php $view->extend('::layout') ?>
Hello <?php echo $name ?>!
# app/views/layout.php <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title> <?php $view['slots']->output('title', 'Hello Application') ?> </title> </head> <body> <?php $view['slots']->output('_content') ?> </body> </html>
étend
Héritage de Vues
_content
layout.php
Hello Hugo!
index.php
# src/Application/HelloBundle/Resources/views/Hello/index.php <?php $view->extend('HelloBundle::layout') ?> Hello <?php echo $name ?>!
# app/views/layout.php <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title> <?php $view['slots']->output('title', 'Hello Application') ?> </title> </head> <body> <?php $view['slots']->output('_content') ?> </body> </html>
# src/Application/HelloBundle/Resources/views/layout.php <?php $view->extend('::layout') ?> <h1>Hello Application</h1> <div> <?php $view['slots']->output('_content') ?> </div>
Héritage multiple
_content
_content Hello Hugo!
index.php
HelloBundle::layout.php
::layout.php
Les Slots sont des fragments dé!nis dans un template et affichés dans un
layout décorant ce dernier.
# app/views/layout.php <html> <head> <title> <?php $view['slots']->output('title', 'Hello Application') ?> </title> </head> <body> <?php $view['slots']->output('_content') ?> </body> </html>
# src/Application/HelloBundle/Resources/views/Hello/index.php <?php $view['slots']->set('title', 'Hello World app') ?>
Symfony fournit des mécanismes simples pour évaluer et inclure des templates
dans un autre # src/Application/HelloBundle/Resources/views/Hello/hello.php Hello <?php echo $name ?>!
# Including another template in the current template <?php echo $view->render('HelloBundle:Hello:hello', array('name' => $name)) ?>
src/Bundle/HelloBundle/Resources/views/Hello/hello.php
Symfony offre également un moyen d’inclure le rendu d’une action depuis
une vue…
# src/Application/HelloBundle/Resources/views/Hello/index.php
<?php $view['actions']->output('HelloBundle:Hello:fancy', array( 'name' => $name, 'color' => 'green’ )) ?>
# src/Application/HelloBundle/Controller/HelloController.php
class HelloController extends Controller { public function fancyAction($name, $color) { // create some object, based on the $color variable $object = ...;
return $this->render('HelloBundle:Hello:fancy', array( 'name' => $name, 'object' => $object )); }
// ... }
Les aides de vue sont des objets accessibles depuis les templates et qui
permettent de simpli!er la logique d’affichage
Générer une URL avec le router helper <a href="<?php echo $view['router']->generate('hello', array( 'name' => 'Thomas')) ?>">Greet Thomas!</a>
Inclure des feuilles de style <head> <!-- ... --> <?php $view['stylesheets']->add('css/styles.css') ?> <?php echo $view['stylesheets'] ?> </head>
Inclure des javascripts
<head> <!-- ... --> <?php $view['javascripts']->add('js/libraries.js') ?> <?php echo $view['javascripts'] ?> </head>
Manipuler des ressource web (images, #ash…)
<img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>" src=""/>
Traduire des chaînes de l’interface
<?php echo $view['translator']->trans('Symfony is %what%!', array( '%what%' => 'awesome')) ?>
Con!guration
3 formats de con!guration
PHP YAML XML
Quel format choisir ? Avantages Inconvénients
XML Validation Complétion dans les EDIs Facile à analyser
Verbeux Long à écrire
YAML Concis Facile à lire Facile à modi!er
Besoin du composant YAML Pas de validation Pas d’autocomplétion
PHP Flexible Plus facile à manipuler
Pas de validation
Con!guration en YML
# app/config/routing.php
homepage: pattern: / defaults: { _controller: FrameworkBundle:Default:index }
hello: resource: HelloBundle/Resources/config/routing.yml
Import d’une autre configura0on
Con!guration en PHP # app/config/routing.php
use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->addRoute('homepage', new Route('/', array( '_controller' => 'FrameworkBundle:Default:index', )));
$collection->addCollection( $loader->import("HelloBundle/Resources/config/routing.php") );
return $collection; Import d’une autre configura0on
Con!guration en XML # app/config/routing.xml
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://www.symfony-project.org/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.symfony-project.org/schema/routing http://www.symfony-project.org/schema/routing/routing-1.0.xsd">
<route id="homepage" pattern="/"> <default key="_controller">FrameworkBundle:Default:index</default> </route>
<import resource="HelloBundle/Resources/config/routing.xml" /> </routes>
Import d’une autre configura0on
Import de !chiers INI # app/config/config_dev.yml imports: - { resource: config.yml } - { resource: custom.ini}
zend.logger: priority: debug path: %kernel.root_dir%/logs/%kernel.environment%.log
# app/config/custom.ini [parameters] dice.min = 1 dice.max = 6
public function diceAction() { // ...
$min = (int) $this->container->getParameter('dice.min'); $max = (int) $this->container->getParameter('dice.max');
// ... }
Accès à la con!guration depuis le code
Outils de Débogage
Parce qu’il est important pour un développeur d’identi!er rapidement les
bogues et les problèmes !!!
Web Debug Toolbar
Logs
Trace de l’excep0on courrante
Trace pour une InvalidArgumentExcep0on
404 Status Code
Traces d’exception
Afficher / masquer la trace d’une excep0on
Logs enregistrés
Lien vers le profiler
Pro!ler
Trace de l’excep0on
Recherche dans les logs
Requêtes SQL
Extensibilité
http://www.symfony2bundles.org
# app/AppKernel.php class AppKernel extends Kernel { // ... public function registerBundles() { $bundles = array( // ... // Register third party bundles new Bundle\TwitterBundle\TwitterBundle(), new Bundle\ForumBundle\ForumBundle() );
// ... return $bundles; } }
Enregistrement de bundles
DBAL & ORM Doctrine2
• Abstraction de base de données relationnelles • Performance • Plus de magie • Manipulation de vrais objets PHP (POPO) • Génération de code
• Adapteur MongoDB disponible
๏ Con!gurer la connexion BDD en YAML
# app/config/config.yml doctrine.dbal: dbname: Blog user: root password: ~
doctrine.orm: ~
// web/.htaccess or in the vhost configuration
SetEnv SYMFONY__DOCTRINE__DBAL__USERNAME "root" SetEnv SYMFONY__DOCTRINE__DBAL__PASSWORD "secret"
๏ Con!gurer la connexion BDD dans Apache
๏ Dé!nition d’une entité (table) à l’aide d’une classe PHP
namespace Application\BlogBundle\Entity;
/**
* @Entity(repositoryClass="Application\BlogBundle\Model\BlogPostRepository")
* @Table(name="blog_post")
*/
class BlogPost {
/**
* @Id @Column(type="integer")
* @GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/** @Column(length=100) */
protected $title;
/** @Column(type="text") */
protected $content;
}
Annota0ons
๏ Génération de la base de données à partir des classes PHP
๏ Chargement des données de test
$ php app/console doctrine:database:create $ php app/console doctrine:schema:create
$ php app/console doctrine:data:load
# src/Application/BlogBundle/Resources/data/fixtures/doctrine/fixtures.php
use Application\BlogBundle\Entity\BlogPost;
$post1 = new BlogPost(); $post1->setTitle('My first blog post'); $post1->setContent('Lorem ipsum dolor sit amet...');
$post2 = new BlogPost(); $post2->setTitle('My second blog post'); $post2->setContent('Lorem ipsum dolor sit amet...');
$post3 = new BlogPost(); $post3->setTitle('My third blog post'); $post3->setContent('Lorem ipsum dolor sit amet...');
๏ Les données de test sont écrites en pur PHP
# src/Application/BlogBundle/Model/BlogPostRepository.php namespace Application\BlogBundle\Model; use Doctrine\ORM\EntityRepository;
class BlogPostRepository extends EntityRepository { public function getHomepagePosts() { $query = $this->_em->createQuery(' SELECT u FROM BlogBundle:BlogPost u ORDER BY u.id DESC ');
return $query->getResult(); } }
๏ Ecrire des requêtes DQL dans un modèle Doctrine
# src/Application/BlogBundle/Controller/BlogController.php // ... class BlogController extends Controller { public function indexAction() { $em = $this['doctrine.orm.entity_manager'];
$posts = $em->getRepository('BlogBundle:BlogPost') ->getHomepagePosts();
return $this->render('BlogBundle:Blog:index', array( 'posts' => $posts )); }
// ... }
๏ Interroger la base de données à l’aide du Modèle Doctrine
Emails Swift Mailer
• API Orientée Objet Open-Source • Support des connexions SMTP • Support des pièces jointes • Support des formats de mails (text, html…) • Gestion des !les d’attente (spools) • Facile à con!gurer et à étendre avec des plugins
Con!gurer Swift Mailer # app/config/config.yml
swift.mailer: transport: smtp encryption: ssl auth_mode: login host: smtp.gmail.com username: your_username password: your_password
Envoyer un Email public function indexAction($name) { $mailer = $this['mailer'];
$message = \Swift_Message::newInstance() ->setSubject('Hello Email') ->setFrom('[email protected]') ->setTo('[email protected]') ->setBody($this->renderView('HelloBundle:Hello:email', array( 'name' => $name )));
$mailer->send($message);
return $this->render(...); }
Généra0on du corps du mail à l’aide d’un template et de la méthode
renderView()
Récupéra0on du service d’envoi de mails
Tests Automatisés PHPUnit
• Tests Unitaires et Couverture de Code
• Garantir la qualité du code • Eviter les bugs et les régressions • Documenter le code
• Industrialiser et professionnaliser les développements
# src/Application/BlogBundle/Tests/Entity/BlogPostTest.php namespace Application\BlogBundle\Tests\Entity; use Application\BlogBundle\Entity\BlogPost;
class BlogPostTest extends \PHPUnit_Framework_TestCase { public function testTitleIsSlugifiedOnce() { $slug = 'symfony2-rules-the-world';
$post = new BlogPost(); $post->setTitle('Symfony2 rules the world'); $this->assertEquals($slug, $post->getSlug());
// Slug doesn't change when it's already set $post->setTitle('An other title'); $this->assertEquals($slug, $post->getSlug()); } }
๏ Exemple de script de tests unitaires dans Symfony2
• Tests fonctionnels
• Simuler des scénarios de navigation • Simuler un client Web (navigateur)
• Véri!er que l’application respecte le cahier des charges
class BlogControllerTest extends WebTestCase { // ... public function testAddComment() { $this->client->followRedirects();
$crawler = $this->client->request('GET', '/');
// Get the first link to a post $link = $crawler->filter('h2.post a')->first()->link();
// Click on the link and check there are two comments $crawler = $this->client->click($link); $this->assertTrue($crawler->filter('.comment')->count() == 2); } }
๏ Exemple de script de tests fonctionnels dans Symfony2
$crawler = $client->request('GET', '/hello/Fabien');
๏ Simuler des requêtes GET
๏ Simuler des requêtes POST $client->request('POST', '/submit', array('name' => 'Fabien')
$client->request('POST', '/submit', array('name' => 'Fabien'), array('photo' => '/path/to/photo') );
๏ Simuler des uploads de !chiers en POST
$client->request('DELETE', '/post/12', array(), array(), array( 'PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word' ));
๏ Simuler une requête HTTP DELETE avec des entêtes
๏ Désactiver / activer les redirections HTTP
$client->followRedirects(false);
$client->followRedirect();
$client->insulate();
๏ Insoler le client dans un processus séparé
$client->back();
$client->forward();
$client->reload();
๏ Naviguer dans l’historique comme dans un navigateur web
๏ Réinitialiser le Client
$client->restart();
๏ Parcourir le DOM avec le DOM Crawler
// Nodes that match the CSS selector $crawler->filter('h1');
// Nodes that match the XPath expression $crawler->filterXpath('h1');
// Node for the specified index $crawler->eq(1);
// First node $crawler->first();
// Last node $crawler->last();
// Siblings $crawler->siblings();
// All following siblings $crawler->nextAll();
// All preceding siblings $crawler->previousAll();
// Parent nodes $crawler->parents();
// Children $crawler->children();
// Nodes for which the callable, a lambda, returns true $crawler->reduce($lambda);
// Returns the attribute value for the first node $crawler->attr('class');
// Returns the node value for the first node $crawler->text();
// Extracts an array of attributes for all nodes // (_text returns the node value) $crawler->extract(array('_text', 'href'));
// Executes a lambda for each node // and return an array of results $data = $crawler->each(function ($node, $i) {
return $node->getAttribute('href'); });
๏ Extraire des données sur des noeuds
๏ Simuler des clics sur des liens ou boutons $crawler->selectLink('Click here');
$link = $crawler->link(); $client->click($link);
$links = $crawler->links();
๏ Poster des formulaires
// Select the submit button of a form $crawler->selectButton('submit');
// Get a form instance $form = $crawler->form();
// Override the default form values $form = $crawler->form(array( 'name' => 'Fabien', 'like_symfony' => true, ));
Performances
• PHP 5.3.2 minimum • “ Cachy framework “ • Cache HTTP & Proxy cache (ESI)
• Faible consommation mémoire • Tous les services sont chargés à la demande
If the standalone parameter is set to false, Symfony2 will render the
HTML content
// src/Application/BlogBundle/Resources/views/layout.php
$view['actions']->output('BlogBundle:Blog:lastComments', array(), array( 'standalone' => false ));
<esi:include src="..." /> If the standalone parameter is set to true and if there is a compa0ble proxy cache, Symfony2 will render an ESI tag
// src/Application/BlogBundle/Resources/views/layout.php
$view['actions']->output('BlogBundle:Blog:lastComments', array(), array( 'standalone' => true ));
Edge Side Includes aka ESI...
Edge Side Includes
Questions ?
Trainings Business Unit [email protected]
Sensio S.A. 92-98, boulevard Victor Hugo
92 115 Clichy Cedex FRANCE
Tél. : +33 1 40 99 80 80
www.sensiolabs.com - www.symfony-project.org - trainings.sensiolabs.com
Recommended