Successfully reported this slideshow.
Your SlideShare is downloading. ×

Symfony2 en pièces détachées

Ad

Symfony2 en pièces détachées
   Symfony Live 2011 - 4 mars 2011 - Paris

Ad

Hugo Hamon aka @hhamon


² Responsable des formations Sensio Labs

² Ancien membre du Bureau de l’AFUP
² Auteur d’ouvra...

Ad

Avant d’oublier…




  http://joind.in/talk/view/2766

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Upcoming SlideShare
Neuperl6
Neuperl6
Loading in …3
×

Check these out next

1 of 125 Ad
1 of 125 Ad

Symfony2 en pièces détachées

Download to read offline

Paradoxalement, Symfony2 n'est pas qu'un framework "full-stack". Il s'agit avant tout d'une parfaite synergie de briques logicielles autonomes qui travaillent de concert sous la baguette d'un seul chef d'orchestre : le conteneur d'injection de dépendances. Mais savez-vous que vous pouvez aussi les utiliser sans le framework ? Tous ces composants indépendants sont distribués sous licence MIT et offrent aux développeurs la liberté de les utiliser dans leurs projets PHP. Au cours de cette session, nous mettrons en lumière les fonctionnalités offertes par les principaux composants de Symfony2 tels que DependencyInjection, Console, Finder, EventDispatcher, Translation et bien d'autres encore. Vous découvrirez comment les intégrer et les utiliser dans vos projets PHP, et ainsi devenir le prochain Maestro du web.

Paradoxalement, Symfony2 n'est pas qu'un framework "full-stack". Il s'agit avant tout d'une parfaite synergie de briques logicielles autonomes qui travaillent de concert sous la baguette d'un seul chef d'orchestre : le conteneur d'injection de dépendances. Mais savez-vous que vous pouvez aussi les utiliser sans le framework ? Tous ces composants indépendants sont distribués sous licence MIT et offrent aux développeurs la liberté de les utiliser dans leurs projets PHP. Au cours de cette session, nous mettrons en lumière les fonctionnalités offertes par les principaux composants de Symfony2 tels que DependencyInjection, Console, Finder, EventDispatcher, Translation et bien d'autres encore. Vous découvrirez comment les intégrer et les utiliser dans vos projets PHP, et ainsi devenir le prochain Maestro du web.

Advertisement
Advertisement

More Related Content

Advertisement

Symfony2 en pièces détachées

  1. 1. Symfony2 en pièces détachées Symfony Live 2011 - 4 mars 2011 - Paris
  2. 2. Hugo Hamon aka @hhamon ² Responsable des formations Sensio Labs ² Ancien membre du Bureau de l’AFUP ² Auteur d’ouvrage PHP / Symfony chez Eyrolles ² http://www.hugohamon.com
  3. 3. Avant d’oublier… http://joind.in/talk/view/2766
  4. 4. Quelques questions… Qui développe encore des applications sans framework ou bibliothèques ?
  5. 5. Quelques questions… Qui réutilise des composants éprouvés ? Zend Framework, PEAR, EZ Components…
  6. 6. Quelques questions… Qui réinvente à la roue ?
  7. 7. Symfony2 est bâti autour d’une bibliothèque de composants indépendants
  8. 8. Les composants §  BrowserKit §  HTTP Kernel §  ClassLoader §  Locale §  Console §  Process §  CSS Selector §  Routing §  Dependency Injection §  Security §  Dom Crawler §  Serializer §  Event Dispatcher §  Templating §  Finder §  Translation §  Form §  Validator §  HTTP Foundation §  YAML
  9. 9. ClassLoader ClassLoader
  10. 10. ClassLoader UniversalClassLoader
  11. 11. ClassLoader UniversalClassLoader use SymfonyComponentClassLoaderUniversalClassLoader; $loader = new UniversalClassLoader(); $loader->registerNamespaces(array( 'Symfony' => __DIR__ .'/symfony/src', 'Doctrine' => __DIR__ .'/doctrine/lib', 'Zend' => __DIR__ .'/zend/library', )); ty ro perabili PHP 5.3 Inte d $loader->register(); standar
  12. 12. ClassLoader UniversalClassLoader use SymfonyComponentClassLoaderUniversalClassLoader; $loader = new UniversalClassLoader(); $loader->registerNamespaces(array( 'Sensio' => array(__DIR__ .'/src', __DIR__.'/sensio-extra') 'Symfony' => __DIR__ .'/symfony/src', 'Doctrine' => __DIR__ .'/doctrine/lib', 'Zend' => __DIR__ .'/zend/library', )); $loader->register(); Fallback
  13. 13. ClassLoader UniversalClassLoader use SymfonyComponentClassLoaderUniversalClassLoader; $loader = new UniversalClassLoader(); $loader->registerPrefixes(array( 'Swift_' => __DIR__ .'/swiftmailer/lib/classes', 'Twig_' => __DIR__ .'/twig/lib/classes', )); $loader->register(); le P EAR sty
  14. 14. HttpFoundation HttpFoundation
  15. 15. HttpFoundation Request
  16. 16. HttpFoundation Classe Dé nition Gestion des entêtes et paramètres GET, POST, Cookie, Request Server… Response Gestion des entêtes et contenu de la réponse Session Gestion de la session PHP Cookie Génère un nouveau cookie File Gestion des chiers
  17. 17. HttpFoundation Request require_once __DIR__ .'/../src/autoload.php'; use SymfonyComponentHttpFoundationRequest; $request = Request::createFromGlobals(); $name = $request->query->get('name'); // Get query string variable $name = $request->request->get('name'); // Get post variable $name = $request->cookies->get('name'); // Get cookie value $file = $request->files->get('avatar'); // Get uploaded file $method = $request->getMethod(); // Get the request method $ip = $request->getClientIp(); // Get remote address $https = $request->isSecure(); // True if HTTPS $ajax = $request->isXmlHttpRequest(); // True if ajax request
  18. 18. HttpFoundation Session
  19. 19. HttpFoundation Session §  Lecture et écriture de variables de session §  Lecture et écriture de messages ash §  Invalidation et nettoyage de la session §  Choix du moteur de stockage § ArraySessionStorage : session non persistente § NativeSessionStorage : session PHP native § PdoSessionStorage : session en base de données
  20. 20. HttpFoundation Session native de PHP use SymfonyComponentHttpFoundationSession; use SymfonyComponentHttpFoundationSessionStorageNativeSessionStorage; $session = new Session(new NativeSessionStorage()); $session->start(); // Read the session id $id = $session->getId(); // Set a session variable $session->set('username', 'hhamon'); // Set a flash message $session->setFlash('notice', 'You win!');
  21. 21. HttpFoundation Session native de PHP use SymfonyComponentHttpFoundationSession; use SymfonyComponentHttpFoundationSessionStorageNativeSessionStorage; $session = new Session(new NativeSessionStorage()); $session->start(); // Read the session variable echo $session->get('username'); // Read a flash message if ($session->hasFlash('notice')) { echo $session->getFlash('notice'); } // Remove a session variable $session->remove('username'); $session->clear();
  22. 22. HttpFoundation Session en base de données §  Utilisation des objets PdoSessionStorage et PDO require_once __DIR__ .'/../src/autoload.php'; use SymfonyComponentHttpFoundationSession; use SymfonyComponentHttpFoundationSessionStoragePdoSessionStorage; $pdo = new PDO('mysql:host=localhost;dbname=sflive2011', 'root', '', array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION )); $storage = new PdoSessionStorage($pdo, array('db_table' => 'php_session')); $session = new Session($storage, array('default_locale' => 'fr')); $session->start(); $session->set('username', 'hhamon');
  23. 23. HttpFoundation Session en base de données §  Création de la table dans la base de données CREATE TABLE `php_session` ( `sess_id` varchar(40) COLLATE utf8_unicode_ci NOT NULL, `sess_data` text COLLATE utf8_unicode_ci, `sess_time` INTEGER(10) UNSIGNED NOT NULL, PRIMARY KEY (`sess_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; §  Contrôle des données de session dans la table
  24. 24. HttpFoundation Response
  25. 25. HttpFoundation Response §  Support des entêtes HTTP §  Content-Type §  Status-Code §  Support du cache HTTP §  Expires §  Etag §  Cache-Control (max-age, s-maxage…) §  Support des cookies
  26. 26. HttpFoundation Response - Headers use SymfonyComponentHttpFoundationResponse; $response = Response('<html>...</html>', 200); $response->headers->set('Content-Type', 'text/html; charset=utf-8'); $response->setEtag('article-1337'); $response->setSharedMaxAge(3600); $response->send(); HTTP/1.0 200 OK cache-control: s-maxage=3600 content-type: text/html; charset=utf-8 etag: "article-1337" <html>...</html>
  27. 27. HttpFoundation Cookie
  28. 28. HttpFoundation Response - Cookie require_once __DIR__ .'/../src/autoload.php'; use SymfonyComponentHttpFoundationResponse; use SymfonyComponentHttpFoundationCookie; $cookie = new Cookie('last-viewed', '1337', time()+3600*24); $response = new Response('<html>...</html>', 200); $response->headers->setCookie($cookie); $response->send(); HTTP/1.0 200 OK content-type: text/html; charset=utf-8 set-cookie: last-viewed=1337; expires=Mon, 10-Jan-2011 00:52:24 GMT; path=/; httponly <html>...</html>
  29. 29. HttpFoundation File
  30. 30. HttpFoundation Fichiers et uploads use SymfonyComponentHttpFoundationFileFile; // Set the document root File::setDocumentRoot(__DIR__); $file = new File(__DIR__.'/../tmp/avatar.png'); // Change the file location & rename it on the fly $file->move(__DIR__.'/images', 'hhamon.png'); $ext = $file->getExtension(); // Get the file extension $size = $file->getMimeType(); // Get the file type $path = $file->getPath(); // Get the file path echo '<img src="', $file->getWebPath() ,'" alt="" />';
  31. 31. HttpFoundation Fichier et uploads §  Uploader un chier avec Request et UploadedFile require __DIR__ .'/../src/autoload.php'; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpFoundationFileUploadedFile; $request = Request::createFromGlobals(); $avatar = $request->files->get('avatar'); if ($avatar instanceOf UploadedFile && UPLOAD_ERR_OK == $avatar->getError()) { $avatar->move(__DIR__.'/images'); } <form action="upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="avatar"/> <button type="submit">upload</button> </form>
  32. 32. Event Dispatcher Event Dispatcher
  33. 33. Event Dispatcher Introduction §  Implémentation du pattern « Observateur » §  2 classes et 2 interfaces §  Faciliter le découplage entre les classes §  Améliorer la exibilité / réutilisabilité du code §  Simpli er l’intégration de plugins tiers
  34. 34. Event Dispatcher La classe EventDispatcher §  Connecte des écouteurs (« callables ») à des événements $dispatcher = new EventDispatcher(); $dispatcher->connect('the.event.name', $callable); §  Les sujets « noti ent » des événements aux écouteurs class Subject { public $dispatcher; public function doThings() { $event = new Event($this, 'the.event.name'); $this->dispatcher->notify($event); } }
  35. 35. Event Dispatcher « Forcer le nettoyage d’un cache lorsque certains événements se produisent »
  36. 36. Event Dispatcher Vider un cache class WikiPage { public $id; public $title; public $content; public function save(PDO $conn) { if (!$this->id) { $conn->query('INSERT INTO ...'); } else { $conn->exec('UPDATE ...'); } } En cas de modi cation, la page html } statique du cache doit être régénérée…
  37. 37. namespace MyDomain; use SymfonyComponentEventDispatcherEventDispatcher; use SymfonyComponentEventDispatcherEvent; class WikiPage { // ... private $dispatcher; public function __construct(EventDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } public function save(PDO $conn) { if (!$this->id) { Un événement est noti é pour // ... demander aux écouteurs de } else { vider le cache de l’objet. $conn->exec('UPDATE ...'); $args = array('type' => 'wiki', 'id' => $this->id); $event = new Event($this, 'cache.flush', $args); $this->dispatcher->notify($event); } } }
  38. 38. namespace MyCache; use SymfonyComponentEventDispatcherEvent; class CacheManager { private $cacheDir; public function __construct($cacheDir) { $this->cacheDir = $cacheDir; Ecouteur invoqué à la } noti cation de l’événement. public function listenToCacheFlushEvent(Event $event) { // The WikiPage object $object = $event->getSubject(); // Retrieve all extra arguments $args = $event->all(); $path = $this->cacheDir .'/'. $args['type'] .'/'. $args['id'] .'.html'; if (file_exists($path)) { @unlink($path); } } }
  39. 39. use SymfonyComponentEventDispatcherEventDispatcher; use MyDomainWikiPage; use MyCacheCacheManager; $pdo = new PDO('...'); $cache = new CacheManager('/path/to/cache'); # Register a new listener. $dispatcher = new EventDispatcher(); $dispatcher->connect('cache.flush', array($cache, 'listenToCacheFlushEvent')); # Find the wiki page object with its id. $page = WikiPage::findById($_GET['id']); $page->setEventDispatcher($dispatcher); # Page properties are edited. $page->title = 'New title'; $page->content = 'New content'; # The CacheManager::listenToCacheFlushEvent method is called. $page->save($pdo);
  40. 40. Event Dispatcher « Filtrer / transformer une valeur »
  41. 41. Event Dispatcher Filtrer une donnée class WikiPage { private $rawContent; private $parsedContent; public function setRawContent($rawContent) { $this->rawContent = $rawContent; $parser = new WikiParser(); $this->parsedContent = $parser->parse($rawContent); } }
  42. 42. Event Dispatcher Problème ? §  Les objets « WikiPage » et « WikiParser » sont fortement couplés §  Changer le parser implique de modi er la classe « WikiPage » §  Idem, si l’on veut ajouter des ltres supplémentaires §  Testabilité du code réduite due à la dépendance
  43. 43. Event Dispatcher Solution? §  Filtrer la valeur en appelant une série de ltres (« écouteurs »). class WikiPage { private $rawContent; private $parsedContent; public function setRawContent($rawContent) { $$this->rawContent = $rawContent; $event = new Event('wiki.filter_content', $rawContent); $this->parsedContent = $this->dispatcher->filter($event); } }
  44. 44. use SymfonyComponentEventDispatcherEventDispatcher; use MyDomainWikiPage; use MyFilterWikiSyntaxParser; $dispatcher = new EventDispatcher(); # Register a first filter $dispatcher->connect('wiki.filter_content', function(Event $e, $value) { return striptags($value, '<code>') }); $wikiParser = new WikiSyntaxParser(); # Register a second filter $dispatcher->connect('wiki.filter_content', array($wikiParser, 'parse')); $wikiPage = new WikiPage(); $wikiPage->setEventDispatcher($dispatcher); # Set and filter the raw content value $wiki->setRawContent('Some **wiki** content with <script>alert("foo")</script> <code>$c = $a + $b</code>.');
  45. 45. Event Dispatcher Filtrer une valeur §  La propriété « parsedContent » aura la valeur suivante. Some <strong>wiki</strong> content with <code>$c = $a + $b</code>. §  La balise <script> a été ltrée §  La balise <code> a été conservée §  La syntaxe Wiki a été transformée en HTML
  46. 46. Dependency Injection Dependency Injection
  47. 47. Dependency Injection Introduction §  « Inversion de contrôle » §  Composant au cœur de Symfony2 §  Instanciation et initialisation de services à la demande §  Supporte les dépendances entre les différents services §  Dé nition des services en PHP, XML ou YAML
  48. 48. Dependency Injection Dé nition d’un service SOAP # config/services.xml <?xml version="1.0" ?> <container xmlns="http://www.symfony-project.org/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http:// www.symfony-project.org/schema/dic/services/services-1.0.xsd"> <parameters> <parameter key="soap_server.wsdl.uri"> http://www.components.local/soap-server.php?wsdl </parameter> <parameter key="soap_server.response.return">true</parameter> <parameter key="soap_server.options.encoding">utf-8</parameter> <parameter key="calculator_service.class">ApplicationCalculator</parameter> </parameters> <services> <!-- ... --> Paramètres  de  configura1on  des  services.   </services> </container>
  49. 49. Dependency Injection Dé nition d’un service SOAP <?xml version="1.0" ?> <container ... > <!-- ... --> <services> <!-- SOAP Server --> Défini1on  d’un  serveur  Zend   <service id="soap_server" class="ZendSoapServer"> <argument>%soap_server.wsdl.uri%</argument> Soap  comme  étant  un  service   <argument type="collection"> <argument key="encoding">%soap_server.options.encoding%</argument> </argument> <call method="setReturnResponse"> <argument>%soap_server.response.return%</argument> </call> <call method="setObject"><argument type="service" id="calculator_service" /></call> </service> <!-- SOAP Autodiscovery --> <service id="soap_autodiscover" class="ZendSoapAutodiscover"> <call method="setClass"><argument type="string">%calculator_service.class%</argument></call> </service> <!-- Calculator service used by the SOAP server --> <service id="calculator_service" class="%calculator_service.class%"/> </services> </container>
  50. 50. Dependency Injection Dé nition d’un service SOAP <!-- Identification du service : identifiant + type --> <service id="soap_server" class="ZendSoapServer"> <!-- 1er argument du constructeur : valeur d’un paramètre défini précédemment --> <argument>%soap_server.wsdl.uri%</argument> <!-- 2nd argument du constructeur : tableau associatif d’options --> <argument type="collection"> <argument key="encoding">utf-8</argument> </argument> <!– 1ère méthode à appeler juste après l’instanciation du service --> <call method="setReturnResponse"> <argument>true</argument> </call> <!– 2nd méthode à appeler avec une instance du service calculator_service en argument --> <call method="setObject"> <argument type="service" id="calculator_service" /> </call> </service>
  51. 51. Dependency Injection Dé nition du service Calculator namespace Application; class Calculator { /** Documenta1on  avec  de  la  PHPDoc  afin  de   * Returns the sum of two numbers. générer  le  WSDL  correspondant  grâce  à   * Zend_Soap_Autodiscover   * @param float $a The first operand * @param float $b The second operand * @return float The result * @throws SoapFault */ public function sum($a, $b) { if (!is_numeric($a) || !is_numeric($b)) { throw new SoapFault('The sum() operation only accepts numeric arguments.'); } return $a + $b; } }
  52. 52. Dependency Injection Obtenir le conteneur de services # web/soap-server.php use SymfonyComponentConfigContainerBuilder; use SymfonyComponentDependencyInjectionContainerBuilder; use SymfonyComponentDependencyInjectionParameterBagParameterBag; use SymfonyComponentDependencyInjectionLoaderXmlFileLoader; $container = new ContainerBuilder(new ParameterBag()); $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader->load('services.xml'); Lecture  du  fichier  services.xml  et   if (isset($_GET['wsdl'])) { $autodiscover = $container->get('soap_autodiscover'); chargement  de  la  défini1on  dans  le   $response = $autodiscover->toXml(); conteneur  d’injec1on  de  dépendances   } else { $soapServer = $container->get('soap_server'); $response = $soapServer->handle(); } Récupéra1on  du  serveur  SOAP  Zend   header('Content-Type: text/xml'); correctement  instancié  et  ini1alisé.   echo $response;
  53. 53. Finder Finder
  54. 54. Finder Rechercher des chiers §  Rechercher des chiers dans une arborescence §  Filtres sur des critères §  Type ( chiers ou répertoires) §  Nom et extension (patterns) §  Taille §  Date §  … §  Tri les résultats sur des attributs de chiers
  55. 55. Finder Rechercher des chiers use SymfonyComponentFinderFinder; $finder = new Finder(); // All files in the current folder $files = $finder->files()->in(__DIR__); // All directories in the current folder $files = $finder->directories()->in(__DIR__); // All PHP files in the current folder only $files = $finder->files()->name('/.php$/')->depth(0)->in(__DIR__); // All files whose size is between 1K and 2K inclusive $files = $finder->files()->size('>= 1K')->size('<= 2K')->in(__DIR__);
  56. 56. Finder Rechercher des chiers // Search files in several folders $files = $finder->files()->in(array(__DIR__, __DIR__.'/../src')); // Sort file by name $files = $finder->files()->name('/.php$/')->in(__DIR__)->sortByName(); // Sort files by type $files = $finder->files()->name('/.php$/')->in(__DIR__)->sortByType(); // Filter by date $files = $finder->files()->name('/.(png|jpg|jpeg|gif)$/i')-> date('>= 2011-01-09')-> date('<= 2011-02-03')->in(__DIR__); $files = $finder->files()->name('/.(png|jpg|jpeg|gif)$/i')-> date('since 2011-01-09')-> date('before 2011-02-03')->in(__DIR__);
  57. 57. Finder Filtres personnalisés # Create a lambda function that acts as a filter. $onlyWritableFiles = function(SplFileInfo $file) { return $file->isWritable(); }; Un ltre personnalisé est une fonction anonyme qui reçoit une instance de # Find writeable TXT files $finder = new Finder(); SplFileInfo et retourne une valeur booléenne. $files = $finder->files()-> name('/.txt$/')-> filter($onlyWritableFiles)-> in(__DIR__.'/../cache'); foreach (iterator_to_array($files) as $path => $file) { echo $file->getFilename(); }
  58. 58. Templating Templating
  59. 59. Templating Introduction §  Système de templating simple et efficace §  Supporte des moteurs de stockage différents §  Fichiers §  Chaînes de caractères §  Supporte des fonctionnalités « modernes » §  Héritage de templates §  Slots §  Aide de vue (« helpers ») §  Extensible
  60. 60. Templating Génération d’un template simple §  Chargement et rendu d’un template simple require_once __DIR__ .'/../src/autoload.php'; use SymfonyComponentTemplatingPhpEngine; use SymfonyComponentTemplatingTemplateNameParser; use SymfonyComponentTemplatingLoaderFilesystemLoader; $loader = new FilesystemLoader(array( __DIR__.'/../src/tpl/%name%.php', Mo1fs  de  chemins  de   __DIR__.'/../src/tpl/hello/%name%.php', fichiers  de  templates   )); $engine = new PhpEngine(new TemplateNameParser(), $loader); echo $engine->render('index', array('name' => 'Hugo')); Variables  du  template  
  61. 61. Templating Génération d’un template simple §  La variable $view correspond à l’objet $engine précédent. # src/tpl/index.php <p> Hello <?php echo $view->escape($name) ?>! </p> §  L’échappement des variables est assuré par la méthode escape() de l’objet $view.
  62. 62. Templating Génération d’un template avancé use SymfonyComponentTemplatingPhpEngine; use SymfonyComponentTemplatingTemplateNameParser; use SymfonyComponentTemplatingLoaderFilesystemLoader; use SymfonyComponentTemplatingHelperSlotsHelper; $loader = new FilesystemLoader(array( Support des aides de vue et __DIR__.'/../src/tpl/%name%.php', __DIR__.'/../src/tpl/hello/%name%.php', de l’héritage de templates. )); $engine = new PhpEngine(new TemplateNameParser(), $loader, array( new SlotsHelper() )); $content = $engine->render('index', array('name' => 'Hugo')); echo $content;
  63. 63. Templating Génération d’un template avancé # src/tpl/index.php Extension du template de base <?php $view->extend('layout') ?> <?php $view['slots']->set('title', 'Hello Application') ?> <p> Hello <?php echo $view->escape($name) ?>! Dé nition de la valeur d’un slot </p> # src/tpl/layout.php <html> <head> <title> <?php $view['slots']->output('title', 'Templating component') ?> </title> </head> <body> <h1>Welcome!</h1> Affichage du slot ou une valeur par <?php $view['slots']->output('_content') ?> défaut si non dé ni </body> </html> Contenu évalué de index.php
  64. 64. Templating Génération d’un template avancé §  Le rendu nal est le suivant : <!DOCTYPE html> <html> <head> <title>Hello Application</title> </head> <body> <h1>Welcome!</h1> <p> Hello Hugo! </p> </body> </html>
  65. 65. Translation Translation
  66. 66. Translation Introduction §  Gestionnaire de dictionnaires de traduction §  Supporte différents formats de stockage §  Array §  PHP §  YAML §  XLIFF §  CSV §  Supporte les chaînes dynamiques §  Embarque des algorithmes de pluralisation
  67. 67. Translation Dé nition d’un dictionnaire XLIFF # i18n/fr/messages.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/ committees/xliff/documents/xliff.dtd"> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="messages.xml"> <header/> <body> <trans-unit id="1"> <source>Hello</source> <target>Salut</target> </trans-unit> <trans-unit id="2"> <source>Hello %name%</source> <target>Bonjour %name%</target> </trans-unit> </body> </file> </xliff>
  68. 68. Translation Traduire des chaînes d’un catalogue require __DIR__ .'/../src/autoload.php'; use SymfonyComponentTranslationTranslator; use SymfonyComponentTranslationMessageSelector; use SymfonyComponentTranslationLoaderXliffFileLoader; // Création d’un objet Translator $translator = new Translator('fr', new MessageSelector()); // Chargement du dictionnaire XLIFF $translator->addLoader('xliff', new XliffFileLoader()); $translator->addResource('xliff', __DIR__.'/../i18n/fr/messages.xml', 'fr'); // Returns "Salut" echo $translator->trans('Hello'); // Returns "Bonjour Hugo" echo $translator->trans('Hello %name%', array('%name%' => 'Hugo'));
  69. 69. Translation Utiliser une locale par défaut §  Dé nition d’un second dictionnaire pour l’allemand # i18n/de/messages.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/ committees/xliff/documents/xliff.dtd"> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="messages.xml"> <header/> <body> <trans-unit id="1"> <source>Welcome in %city%</source> <target>Wilkommen in %city%</target> </trans-unit> </body> </file> </xliff>
  70. 70. Translation Utiliser une locale par défaut §  Le gestionnaire de traduction peut charger plusieurs catalogues de traduction et forcer une locale par défaut. $trans = new Translator('fr', new MessageSelector()); $trans->addLoader('xliff', new XliffFileLoader()); // Load two distinct dictionaries $trans->addResource('xliff', __DIR__.'/../i18n/de/messages.xml', 'de'); $trans->addResource('xliff', __DIR__.'/../i18n/fr/messages.xml', 'fr'); // Set the default fallback locale $trans->setFallbackLocale('de'); // Translation only exists in german catalog even if the locale is fr echo $trans->trans('Welcome in %city%', array('%city%' => 'Paris'));
  71. 71. Translation Pluralisation des chaînes # i18n/fr/messages.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/ committees/xliff/documents/xliff.dtd"> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="messages.xml"> <header/> <body> <!-- ... --> <trans-unit id="3"> <source>{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples</source> <target>{0} Il n'y a pas de pomme|{1} Il y a une pomme|]1,Inf] Il y a %count% pommes</target> </trans-unit> </body> </file> </xliff>
  72. 72. Translation Pluralisation des chaînes §  La méthode transChoice() facilite la pluralisation des chaînes $count = 5; $translator = new Translator('fr', new MessageSelector()); $translator->addLoader('xliff', new XliffFileLoader()); $translator->addResource('xliff', __DIR__.'/../i18n/fr/messages.xml', 'fr'); // Returns "Il y a <strong>5</strong> pommes" echo $translator->transChoice( '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', $count, array('%count%' => '<strong>' . $count . '</strong>') );
  73. 73. Locale Locale
  74. 74. Locale Introduction §  Surcharge la classe Locale de PHP 5.3 §  Supporte les formatages des noms de pays §  Supporte les formatages des noms de langues §  L’API se compose uniquement de méthodes statiques
  75. 75. Locale Obtenir la listes des pays use SymfonyComponentLocaleLocale; print_r(Locale::getDisplayCountries('fr')); print_r(Locale::getDisplayCountries('en')); print_r(Locale::getDisplayCountries('de')); §  Les pays sont retournés dans des tableaux associatifs triés sur les valeurs Array Array Array ( ( ( [AF] => Afghanistan [AF] => Afghanistan [AF] => Afghanistan [ZA] => Afrique du Sud [AL] => Albania [AL] => Albanien [AL] => Albanie [ZA] => South Africa [ZA] => Südafrika ... ... ... ) ) )
  76. 76. Locale Codes ISO des pays use SymfonyComponentLocaleLocale; print_r(Locale::getCountries()); Array ( [0] => AF [1] => AX [2] => AL [3] => DZ [4] => AS [5] => AD [6] => AO [7] => AI [8] => AQ [9] => AG [10] => AR [11] => AM [12] => AW [13] => AC [14] => AU [15] => AT [16] => AZ [17] => BS [18] => BH [19] => BD [20] => BB [21] => BY [22] => BE [23] => BZ [24] => BJ [25] => BM [26] => BT [27] => BO [28] => BA [29] => BW [30] => BV [31] => BR [32] => IO [33] => VG [34] => BN [35] => BG [36] => BF [37] => BI [38] => KH [39] => CM [40] => CA [41] => IC [42] => CV [43] => KY [44] => CF [45] => EA [46] => TD [47] => CL [48] => CN [49] => CX [50] => CP [51] => CC [52] => CO [53] => KM [54] => CG [55] => CD [56] => CK [57] => CR [58] => CI [59] => HR [60] => CU [61] => CY [62] => CZ [63] => DK [64] => DG [65] => DJ [66] => DM [67] => DO [68] => EC [69] => EG [70] => SV [71] => GQ [72] => ER [73] => EE ...)
  77. 77. Locale Obtenir la listes des locales use SymfonyComponentLocaleLocale; print_r(Locale::getDisplayLocales('fr')); print_r(Locale::getDisplayLocales('en')); §  Les locales sont retournées dans des tableaux associatifs triés sur les valeurs Array Array ( ( [fr_BE] => français (Belgique) [fr_BE] => French (Belgium) [fr_CA] => français (Canada) [fr_CA] => French (Canada) [fr_DJ] => français (Djibouti) [fr_DJ] => French (Djibouti) [fr_LU] => français (Luxembourg) [fr_LU] => French (Luxembourg) [fr_CH] => français (Suisse) [fr_CH] => French (Switzerland) ... ... ) )
  78. 78. Locale Codes ISO des locales use SymfonyComponentLocaleLocale; print_r(Locale::getLocales()); Array ( [0] => af [1] => af_NA [2] => ak [3] => sq [4] => am [5] => am_ET [6] => ar [7] => ar_DZ [8] => ar_BH [9] => ar_EG [10] => ar_IQ [11] => ar_JO [12] => ar_KW [13] => ar_LB [14] => ar_LY [15] => ar_MA [16] => ar_OM [17] => ar_QA [18] => ar_SA [19] => ar_SD [20] => ar_SY [21] => ar_TN [22] => ar_AE [23] => ar_YE [24] => hy [25] => as [26] => as_IN [27] => asa [28] => az [29] => az_Cyrl [30] => az_Cyrl_AZ [31] => az_Latn [32] => az_Latn_AZ [33] => bm [34] => eu [35] => be [36] => bem [37] => bez [38] => bn [39] => bn_IN [40] => bs [41] => bg [42] => my [43] => my_MM [44] => ca [45] => tzm [46] => tzm_Latn [47] => tzm_Latn_MA [48] => chr [49] => chr_US [50] => cgg [51] => zh [52] => kw [53] => hr [54] => cs [55] => da [56] => nl [57] => nl_BE [58] => ebu [59] => ebu_KE [60] => en [61] => en_AS [62] => en_AU [63] => en_BE [64] => en_BZ [65] => en_BW [66] => en_CA [67] => en_GU [68] => en_HK [69] => en_IN ...)
  79. 79. Locale Obtenir la listes des langues use SymfonyComponentLocaleLocale; print_r(Locale::getDisplayLanguages('fr')); print_r(Locale::getDisplayLanguages('en')); §  Les langues sont retournées dans des tableaux associatifs triés sur les valeurs Array Array ( ( [en] => anglais [en] => English [en_US] => anglais américain [en_AU] => Australian English [en_AU] => anglais australien [en_GB] => British English [en_GB] => anglais britannique [en_CA] => Canadian English [en_CA] => anglais canadien [en_US] => U.S. English ... ... ) )
  80. 80. Locale Codes ISO des langues use SymfonyComponentLocaleLocale; print_r(Locale::getLanguages()); Array ( [0] => ab [1] => ace [2] => ach [3] => ada [4] => ady [5] => aa [6] => afh [7] => af [8] => afa [9] => ain [10] => ak [11] => akk [12] => sq [13] => ale [14] => alg [15] => tut [16] => am [17] => egy [18] => grc [19] => anp [20] => apa [21] => ar [22] => an [23] => arc [24] => arp [25] => arn [26] => arw [27] => hy [28] => rup [29] => art [30] => as [31] => ast [32] => asa [33] => ath [34] => cch [35] => en_AU [36] => aus [37] => de_AT [38] => map [39] => av [40] => ae [41] => awa [42] => ay [43] => az [44] => ban [45] => bat [46] => bal [47] => bm [48] => bai [49] => bad [50] => bnt [51] => bas [52] => ba [53] => eu [54] => btk [55] => bej [56] => be [57] => bem [58] => bez [59] => bn [60] => ber [61] => bho [62] => bh [63] => bik [64] => bin [65] => bi [66] => byn [67] => zbl [68] => brx [69] => bs [70] => bra [71] => pt_BR [72] => br [73] => en_GB [74] => bug [75] => bg [76] => bua [77] => my [78] => cad [79] => en_CA [80] => fr_CA [81] => yue [82] => car [83] ...)
  81. 81. Console Console
  82. 82. Console Introduction §  Automatiser des tâches pénibles (génération de code…) §  Exécuter des tâches qui prennent du temps §  Béné cier des ressources du serveur §  Plani er des tâches dans une crontab
  83. 83. Console Introduction §  Arguments et options §  Codes de sortie §  Shell §  Coloration de la sortie §  Testabilité §  Messages d’erreur §  …
  84. 84. Console Amorcer le CLI §  Création d’un chier « console.php » exécutable (chmod +x) #!/usr/bin/php <?php require __DIR__ .'/src/autoload.php'; use SymfonyComponentConsoleApplication; $application = new Application('Console', '1.0'); $application->run();  
  85. 85. Console Amorcer le CLI
  86. 86. namespace ApplicationCommand; use SymfonyComponentConsoleCommandCommand; use SymfonyComponentConsoleInputInputArgument; use SymfonyComponentConsoleInputInputOption; use SymfonyComponentConsoleInputInputInterface; use SymfonyComponentConsoleOutputOutputInterface; use ApplicationServiceGoogleWeather; class WeatherCommand extends Command { Dé nition des meta données de la commande : protected function configure() nom, manuel, arguments, options… { $this-> addArgument('city', InputArgument::REQUIRED, 'The city')-> addOption('lang', null, InputOption::VALUE_REQUIRED, 'The lang', 'en')-> setName('google:weather')-> setDescription('Fetches weather information.')-> setHelp(<<<EOF The <info>google:weather</info> command fetches weather data for a given city: <info>google:weather paris --lang fr</info> EOF ); }}
  87. 87. Console Dé nir la logique de la commande $input : arguments + options $output : sortie de la console class WeatherCommand extends Command { # ... protected function execute(InputInterface $input, OutputInterface $output) { $city = $input->getArgument('city'); $lang = $input->getOption('lang'); $weather = new GoogleWeather($city, $lang); $output->writeln(sprintf('<info>City</info>: %s', $weather->getCity())); $output->writeln(sprintf('<info>Temperature</info>: %s', $weather->getTemperature())); $output->writeln(sprintf('<info>Latitude</info>: %s', $weather->getLatitude())); $output->writeln(sprintf('<info>Longitude</info>: %s', $weather->getLongitude())); $output->writeln(sprintf('<info>Condition</info>: %s', $weather->getCondition())); $output->writeln(sprintf('<info>Wind</info>: %s', $weather->getWindCondition())); } }
  88. 88. Console Enregistrer la commande #!/usr/bin/php <?php require __DIR__ .'/src/autoload.php'; use SymfonyComponentConsoleApplication; use ApplicationCommandWeatherCommand; $application = new Application('Console', '1.0'); $application->add(new WeatherCommand()); $application->run();  
  89. 89. Console Consulter la liste des commandes
  90. 90. Console Obtenir de l’aide de la commande ./console help google:weather  
  91. 91. Console Exécuter la commande ./console google:weather Berlin --lang fr  
  92. 92. Process Process
  93. 93. Process Introduction §  Exécuter des lignes de commande depuis un script PHP §  Récupérer la sortie générée par la console §  Récupérer le statut de l’exécution de la console
  94. 94. Process Mise en oeuvre use SymfonyComponentProcessProcess; $process = new Process('ls -lah', __DIR__); $process->run(); if (!$process->isSuccessful()) { $output = $process->getErrorOutput(); } else { $output = $process->getOutput(); } echo $output;
  95. 95. Process Mise en oeuvre total 176 drwxr-xr-x 25 Hugo staff 850B Feb 23 22:53 . drwxr-xr-x 9 Hugo staff 306B Feb 13 19:39 .. -rw-r--r--@ 1 Hugo staff 607B Feb 19 12:48 crawler.php -rw-r--r--@ 1 Hugo staff 1.9K Feb 13 12:08 event-dispatcher.php -rw-r--r--@ 1 Hugo staff 914B Feb 13 13:43 event-dispatcher2.php -rw-r--r--@ 1 Hugo staff 678B Jan 31 21:55 file.php -rw-r--r--@ 1 Hugo staff 1.7K Feb 12 14:40 finder.php drwxrwxrwx 3 Hugo staff 102B Jan 31 21:55 images -rw-r--r--@ 1 Hugo staff 276B Jan 9 23:39 locale.php -rw-r--r--@ 1 Hugo staff 454B Feb 23 22:53 process.php -rw-r--r--@ 1 Hugo staff 746B Jan 31 22:26 read-request.php -rw-r--r--@ 1 Hugo staff 545B Jan 31 22:41 response.php -rw-r--r--@ 1 Hugo staff 1.3K Feb 9 23:58 routing.php -rw-r--r--@ 1 Hugo staff 718B Feb 1 23:38 serializer.php -rw-r--r--@ 1 Hugo staff 502B Jan 9 00:37 session1.php -rw-r--r--@ 1 Hugo staff 682B Jan 9 00:37 session2.php -rw-r--r--@ 1 Hugo staff 520B Jan 9 01:15 session3.php -rw-r--r--@ 1 Hugo staff 241B Feb 6 11:28 soap-client.php -rw-r--r--@ 1 Hugo staff 737B Feb 19 10:45 soap-server.php -rw-r--r--@ 1 Hugo staff 558B Feb 19 10:56 templating-advanced.php -rw-r--r--@ 1 Hugo staff 476B Feb 19 10:56 templating-basic.php
  96. 96. PhpProcess Exécuter un script PHP use SymfonyComponentProcessPhpProcess; $process = new PhpProcess('<?php echo "Hello World!" ?>'); $process->run(); if (!$process->isSuccessful()) { $output = $process->getErrorOutput(); } else { $output = $process->getOutput(); } echo $output;
  97. 97. Serializer Serializer
  98. 98. Serializer Introduction Le composant « Serializer » convertit des objets PHP en représentations XML ou JSON par introspection.
  99. 99. Serializer Architecture
  100. 100. Serializer Architecture §  L’objet « Serializer » transforme un objet PHP en représentation XML (ou JSON) et inversement. §  Les objets « Normalizer » transforment un objet en tableau PHP. §  Les objets « Encoder » transforment un tableau PHP en représentations XML ou JSON.
  101. 101. class Author { private $firstName; private $lastName; public function __construct($firstName, $lastName) { $this->firstName = $firstName; $this->lastName = $lastName; } public function getFirstName() { return $this->firstName; } public function getLastName() { return $this->lastName; } public function getFullName() { return $this->firstName .' '. $this->lastName; } }
  102. 102. class Book { private $title; private $author; public function setTitle($title) { $this->title = $title; } public function getTitle() { return $this->title; } public function setAuthor(Author $author) { $this->author = $author; } public function getAuthor() { return $this->author; } }
  103. 103. Serializer Sérialiser un objet en XML use SymfonyComponentSerializerSerializer; use SymfonyComponentSerializerEncoderXmlEncoder; use SymfonyComponentSerializerNormalizerGetSetMethodNormalizer; $serializer = new Serializer(); # Register the XML encoder $serializer->setEncoder('xml', new XmlEncoder()); # Register a getter / setter normalizer $serializer->addNormalizer(new GetSetMethodNormalizer()); $book = new Book(); $book->setAuthor(new Author('Agatha', 'Christie')); $book->setTitle('Ten Little Niggers'); header('Content-Type: text/xml; charset=utf-8'); echo $serializer->serialize($book, 'xml');
  104. 104. Serializer Sérialiser un objet en XML
  105. 105. Serializer Sérialiser un objet en JSON use SymfonyComponentSerializerSerializer; use SymfonyComponentSerializerEncoderJsonEncoder; use SymfonyComponentSerializerNormalizerGetSetMethodNormalizer; $serializer = new Serializer(); # Register the JSON encoder $serializer->setEncoder('json', new JsonEncoder()); # Register a getter / setter normalizer $serializer->addNormalizer(new GetSetMethodNormalizer()); $book = new Book(); $book->setAuthor(new Author('Agatha', 'Christie')); $book->setTitle('Ten Little Niggers'); header('Content-Type: application/json; charset=utf-8'); echo $serializer->serialize($book, 'json');
  106. 106. Serializer Sérialiser un objet en JSON { "title":"Ten Little Niggers", "author": { "firstname":"Agatha", "lastname":"Christie", "fullname":"Agatha Christie" } }
  107. 107. use SymfonyComponentSerializerSerializer; use SymfonyComponentSerializerEncoderJsonEncoder; use SymfonyComponentSerializerNormalizerGetSetMethodNormalizer; $serializer = new Serializer(); $serializer->setEncoder('json', new JsonEncoder()); $serializer->addNormalizer(new GetSetMethodNormalizer()); $jsonEncoded = '{ "title":"Ten Little Niggers", "author": { "firstname":"Agatha", "lastname":"Christie", "fullname":"Agatha Christie" } }'; header('Content-Type: text/plain; charset=utf-8'); print_r($serializer->decode($jsonEncoded, 'json'));
  108. 108. Serializer Désérialiser un objet JSON Array ( [title] => Ten Little Niggers [author] => Array ( [firstname] => Agatha [lastname] => Christie [fullname] => Agatha Christie ) )
  109. 109. YAML YAML
  110. 110. YAML Introduction YAML signi e « Ain't Markup Language ». C’est un format standard de sérialisation de données pour tous les langages de programmation.
  111. 111. YAML Parser un chier YAML parameters: # Twitter module twitter.timeline.cache: true twitter.auth.username: hhamon twitter.auth.password: s3cr3t # Blog module blog.post.max_per_page: 10 app: available_cultures: [fr, en, de] services: db_connection: class: PDO dsn: mysq:host=localhost;dbname=foo username: root password: ~
  112. 112. YAML Parser un chier YAML §  La classe « Parser » transforme une chaîne YAML en un tableau associatif PHP. use SymfonyComponentYamlParser; $content = file_get_contents(__DIR__ . '/../config/config.yml'); $yaml = new Parser(); $result = $yaml->parse($content); echo '<pre>’; print_r($result); echo '</pre>';
  113. 113. Array ( [parameters] => Array ( [twitter.timeline.cache] => 1 [twitter.auth.username] => hhamon [twitter.auth.password] => s3cr3t [blog.post.max_per_page] => 10 ) [app] => Array ( [available_cultures] => Array ( [0] => fr [1] => en [2] => de ) ) [services] => Array ( [db_connection] => Array ( [class] => PDO [dsn] => mysq:host=localhost;dbname=foo [username] => root [password] => ) ) )
  114. 114. YAML Transformer un tableau en YAML §  La classe « Dumper » transforme un tableau PHP en YAML use SymfonyComponentYamlParser; use SymfonyComponentYamlDumper; $yaml = new Parser(); $result = $yaml->parse(file_get_contents(__DIR__ . '/../config/config.yml')); $result['parameters']['twitter.timeline.cache'] = false; $result['services']['db_connection']['class'] = 'PdoMock'; $dumper = new Dumper(); $data = $dumper->dump($result, 2); file_put_contents(__DIR__ . '/../config/config_test.yml', $data);
  115. 115. YAML Transformer un tableau en YAML parameters: twitter.timeline.cache: false twitter.auth.username: hhamon twitter.auth.password: s3cr3t blog.post.max_per_page: 10 app: available_cultures: [fr, en, de] services: db_connection: { class: PdoMock, dsn: 'mysq:host=localhost;dbname=foo', username: root, password: null }
  116. 116. CSS Selector CSS Selector
  117. 117. CSS Selector CSS3 vers XPath use SymfonyComponentCssSelectorParser; $doc = new DOMDocument(); $doc->loadHTMLFile(__DIR__.'/../src/tpl/page.html'); $xpath = new DOMXPath($doc); $expr = Parser::cssToXpath('ul#menu > li a:contains("Home")'); $xpath->query($expr); foreach ($xpath->query($expr) as $link) { printf('%s > %s', $link->nodeValue, $link->getAttribute('href')); }
  118. 118. CSS Selector CSS3 vers XPath Parser::cssToXpath('ul#menu > li a:contains("Home")'); descendant-or-self::ul[@id = 'menu']/li/descendant::a [contains(string(.), 'Home')]
  119. 119. DomCrawler DomCrawler
  120. 120. DomCrawler Introduction Le composant DomCrawler offre une API simple pour analyser un document SGML grâce au support intégral des sélecteurs CSS3 et XPath.
  121. 121. DomCrawler Introduction §  Analyser et extraire des informations d’un document SGML §  Supporte différentes sources de données Ø Chaîne HTML ou XML Ø Objet DOMNode ou DOMNodeList Ø Objet DOMDocument §  Supporte les sélecteurs CSS3 et Xpath §  Crawler s’appuie sur le composant CssSelector
  122. 122. DomCrawler Instancier un Crawler use SymfonyComponentDomCrawlerCrawler; // from an HTML string $crawler = new Crawler(file_get_contents('http://www.google.com')); // from a DomDocument object $doc = new DOMDocument(); $doc->loadHTML(file_get_contents('http://www.google.com')); $crawler = new Crawler($doc, 'http://www.google.com'); // from a DomNodeList object $list = $doc->getElementsByTagName('ul'); $crawler = new Crawler($list); // from a DomNode object $logo = $doc->getElementById('logo'); $crawler = new Crawler($logo);
  123. 123. DomCrawler Traverser un document /** * <div id="sidebar"> * <h2>Sidebar</h2> * <ul> * <li><a href="home.html">Home</a></li> * <li><a href="about.html">About</a></li> * <li> * <a href="contact.html">Contact</a> * </li> * </ul> * </div> */ $label = $crawler->filter('#sidebar ul li')-> last()->children()-> eq(0)->text();
  124. 124. DomCrawler Traverser un document /** * <div id="sidebar"> * <h2>Sidebar</h2> * <ul> * <li><a href="home.html">Home</a></li> * <li><a href="about.html">About</a></li> * <li> * <a href="contact.html">Contact</a> * </li> * </ul> * </div> */ $links = $crawler->filter('#sidebar ul li a')->links();
  125. 125. The End… Questions ? http://joind.in/talk/view/2766 hugo.hamon@sensio.com @hhamon

×