SlideShare uma empresa Scribd logo
1 de 93
Baixar para ler offline
Découpler votre code pour
assurer la réutilisabilité et
    la maintenabilité
          Fabien Potencier
Fabien Potencier
•  Créateur de Sensio
  –  Agence Web
  –  Depuis 1998
  –  50 collaborateurs
  –  Spécialistes Open-Source
  –  Clients grands comptes / institutionnels


•  Créateur et développeur principal de symfony
  –  Framework PHP
Couplage ?



Coupling is the degree to which
 each program module relies on
 each one of the other modules.

                 http://en.wikipedia.org/wiki/Coupling_(computer_science)
Couplage faible


   Avec un couplage faible, un
 changement dans un module ne
  nécessite pas un changement
dans l’ implementation d’un autre
             module.
Couplage faible



Un module peut intéragir
 avec un autre à travers
  une interface stable
Pourquoi est-ce important ?
Testabilité



La possibilité d’instancier une
 classe dans un test prouve le
  niveau de couplage du code
Readabilité



  On passe plus de temps
à lire et à maintenir du code,
        qu’à en écrire
Maintainabilité


Un changement ici
 ne doit pas causer
un problème là-bas
Extensibilité




Des petits modules indépendants
      facilite son extension
Réutilisabilité
Le monde du développement
     évolue vite… très vite

Mais le but ultime de toutes ces
   évolutions reste le même
Construire de meilleurs
  outils pour éviter de
   réinventer la roue
Construire sur du code / bibliothèques existants
Le spécialiser pour ses propres besoins
… C’est pour ces raisons que l’Open-Source est
 toujours gagnant
La construction d’outils meilleurs est possible
   parce que nous les basons sur des outils
                   existants

      On apprend des projets existants

            On adopte leurs idées

               On les améliore

   C’est le processus naturel de l’évolution
Réutilisabilité

      Configuration
Personnalisation / Extension
 Documentation / Support
Injection de Dépendances
Un exemple Web concret
La plupart des applications doivent stocker les
  préférences des utilisateurs

  –  Langue de l’utilisateur
  –  S’il est authentifié ou non
  –  Ses droits
  –  …
C’est possible avec un objet User

  – setLanguage(), getLanguage()
  – setAuthenticated(), isAuthenticated()
  – addCredential(), hasCredential()
  – ...
La classe User doit trouver un moyen de
 stocker ces données entre les requêtes
                  HTTP

    En PHP, on utilise des sessions
class SessionStorage
{
  function __construct($cookieName = 'PHP_SESS_ID')
  {
    session_name($cookieName);
    session_start();
  }

    function set($key, $value)
    {
      $_SESSION[$key] = $value;
    }

    // ...
}
class User
{
  protected $storage;

    function __construct()
    {
      $this->storage = new SessionStorage();
    }

    function setLanguage($language)
    {
      $this->storage->set('language', $language);
    }

    // ...
}

$user = new User();
Je veux changer le nom du cookie
class User
{                                        Directement
  protected $storage;                    dans la classe
                                             User
    function __construct()
    {
      $this->storage = new SessionStorage('SESSION_ID');
    }

    function setLanguage($language)
    {
      $this->storage->set('language', $language);
    }

    // ...
}

$user = new User();
class User
{
  protected $storage;                           Configuration
                                                  globale ?
    function __construct()
    {
        $this->storage = new SessionStorage(STORAGE_SESSION_NAME);
    }
}



define('STORAGE_SESSION_NAME', 'SESSION_ID');

$user = new User();
Configuration
class User                                  via le
{                                       constructeur
  protected $storage;

    function __construct($sessionName)
    {
      $this->storage = new SessionStorage($sessionName);
    }
}

$user = new User('SESSION_ID');
Configuration
                                          avec un
class User                                tableau
{
  protected $storage;

  function __construct($storageOptions)
  {
    $this->storage = new
 SessionStorage($storageOptions['session_name']);

$user = new User(
   array('session_name' => 'SESSION_ID')
);
Je veux changer le support de stockage des
                 sessions

               Filesystem
                 MySQL
                 SQLite
                    …
Utilisation d’un
class User                             registre global ?
{
  protected $storage;

    function __construct()
    {
      $this->storage = Registry::get('session_storage');
    }
}

$storage = new SessionStorage();
Registry::set('session_storage', $storage);
$user = new User();
La classe User dépend maintenant
       de la classe Registry
Plutôt que de créer l’instance
 Storage dans la classe User

  On injecte la dépendance
  Storage dans l’objet User
Arugment du
class User                            constructeur ?
{
  protected $storage;

    function __construct($storage)
    {
      $this->storage = $storage;
    }
}

$storage = new SessionStorage('SESSION_ID');
$user = new User($storage);
Utilisation de différent support de stockage
class User
{
  protected $storage;
                                     Support de
    function __construct($storage)    stockage
    {                                 différent
      $this->storage = $storage;
    }
}

$storage = new MySQLSessionStorage('SESSION_ID');
$user = new User($storage);
La configuration devient naturelle
class User
{
  protected $storage;

    function __construct($storage)   Configuration
    {                                  naturelle
      $this->storage = $storage;
    }
}

$storage = new MySQLSessionStorage('SESSION_ID');
$user = new User($storage);
Utilisation de classes tierces
     (Interface / Adapter)
class User
{
  protected $storage;

  function __construct(ISessionStorage $storage)
  {
    $this->storage = $storage;
  }                                    Ajout d’une
}                                       interface

interface ISessionStorage
{
  function get($key);

    function set($key, $value);
}
Remplacer l’objet Storage
  pour les tests (Mock)
class User
{
  protected $storage;

    function __construct(ISessionStorage $storage)
    {
      $this->storage = $storage;
    }
}

class SessionStorageForTests implements ISessionStorage
{
  protected $data
  function set($key, $value)
  {                                       Mock de la
    self::$data[$key] = $value;             session
  }
}
Utilisation de différent supports de stockage
          Configuration naturelle
       Intégration de classes tierces
            Simplicité des tests


       Facile sans changement
          de la classe User
C’est l’Injection de
   Dépendance


   Rien de plus
« Dependency Injection is
    where components are given
     their dependencies through
    their constructors, methods,
       or directly into fields. »
http://www.picocontainer.org/injection.html
$storage = new SessionStorage();

// constructor injection
$user = new User($storage);

// setter injection
$user = new User();
$user->setStorage($storage);

// property injection
$user = new User();
$user->storage = $storage;
Un exemple
un peu plus complexe
$request = new WebRequest();
$response = new WebResponse();

$storage = new
 FileSessionStorage('SESSION_ID');
$user = new User($storage);

$cache = new FileCache(
   array('dir' => dirname(__FILE__).'/cache')
);
$routing = new Routing($cache);
class Application
{
  function __construct()
  {
    $this->request = new WebRequest();
    $this->response = new WebResponse();

        $storage = new FileSessionStorage('SESSION_ID');
        $this->user = new User($storage);

        $cache = new FileCache(
           array('dir' => dirname(__FILE__).'/cache')
        );
        $this->routing = new Routing($cache);
    }
}

$application = new Application();
Retour à la case départ
class Application
{
  function __construct()
  {
    $request = new WebRequest();
    $response = new WebResponse();

        $storage = new FileSessionStorage('SESSION_ID');
        $user = new User($storage);

        $cache = new FileCache(
           array('dir' => dirname(__FILE__).'/cache')
        );
        $routing = new Routing($cache);
    }
}

$application = new Application();
On a besoin d’un Container

Description des objets et des relations

            Configuration

               Création
Description d’un
                                      service




$storageDef = new ServiceDefinition(
   'FileSessionStorage'
);



                      Nom de la
                       classe
$storageDef = new ServiceDefinition(
   'FileSessionStorage',
   array('SESSION_ID')
);


         Arugments à
           passer au
          construteur
$container = new ServiceContainer(array(
  'storage' => $storageDef,
));



      Chaque objet a un
       idenfiant unique
$userDef = new ServiceDefinition(‘User’,
   array(new ServiceReference('storage'))
);



                               Référence à un
                                autre object
$storageDef = new ServiceDefinition(
   ‘FileSessionStorage’,
   array(‘SESSION_ID’)
);

$userDef = new ServiceDefinition(‘User’,
   array(new ServiceReference(‘storage’))
);

$container = new ServiceContainer(array(
  'storage' => $storageDef,
  'user'    => $userDef,
));
$user = $container->getService('user');



    Récupération de la configuration de l’objet user

Le user nécessite un objet storage pour son constructeur

   Récupération de la configuration de l’objet storage

     Création de l’objet user en injectant le storage
$user = $container->getService('user');



         Est à peu près équivalent à


$storage = new SessionStorage('SESSION_ID');
         $user = new User($storage);
Un container peu gérer
 n’importe quelle classe (POPO)


     Les objets ne savent pas
qu’ils sont gérés par un container
Astuces d’implémentation
public function getService ($id)
{
  $def = $this->definitions[$id];

    $r = new ReflectionClass($def->getClass());

    if (is_null($r->getConstructor()))
    {
      return $r->newInstance();
    }

    $args = $def->getArguments();
    $args = $this->evaluateArguments($args);

    return $r->newInstanceArgs($args);
}
public function evaluateArguments($arg)
{
  if (is_array($arg))
  {
    return array_map(
       array($this, 'evaluateArguments'), $arg
    );
  }

    if (
      is_object($arg)
      && $arg instanceof ServiceReference
    )
    {
      return $this->getService($arg);
    }

    return $arg;
}
Vers une vraie implémentation
Scope
Quand on récupère un service :
 On veux un nouvel objet à chaque fois ?
  où
 Où le même ?

$userDef->setGlobal(true);

$feedReaderDef->setGlobal(false);
Définition des services
$container = new ServiceContainer(array(               PHP
  'storage' => new ServiceDefinition('FileSessionStorage'),
  'user'    => new ServiceDefinition('User',
                     array(new ServiceReference('storage'))
                  ),
));



<service id="storage" class="FileSessionStorage" />    XML
<service id="user" class="User">
  <constructor_arg type="service" id="storage" />
</service>
Configuration des services
                                     La configuration
<parameter key="storage.class">       est découplée
  SQLiteSessionStorage                de la définition
</parameter>
<parameter key="storage.session_name">
  SESSION_ID
</parameter>


<service id="storage" class="%storage.class%">
  <constructor_arg>
    %storage.session_name%
  </constructor_arg>
</service>
Configuration des services


[storage]
  class = SQLiteSessionStorage
  session_name = SESSION_ID


<service id="storage" class="%storage.class%">
  <constructor_arg>
    %storage.session_name%
  </constructor_arg>
</service>
// Access parameters

$sessionName = $container['storage.session_name'];



// Access services

$user = $container->user;
Imlpémentations en PHP
•  Crafty: http://phpcrafty.sourceforge.net/

•  Garden: http://garden.tigris.org/
                            spring
•  Stubbles: http://www.stubbles.net/wiki/Docs/IOC
                                       Google Guice


•  symfony 2 will have its dependency injection
   framework                            spring
Rappelez-vous que la plupart du
temps, l’utilisation de l’Injecteur
  de Dépendances ne nécessite
 pas l’utilisation d’un Container
Vous pouvez commencer
l’utilisation de l’Injection de
  Dépendance aujourd’hui
En l’implémentant dans vos
            projets

Où en utilisant des bibliothèques
  externes qui utilisent déjà ce
   mécanisme sans Container
symfony
Zend Framework
 ezComponents

   Doctrine
  Swift Mailer
       …
Ajoutons un peu d’i18n
class User
{
  protected $storage, $i18n;

 function __construct($storage, $i18n)
 {
   $this->storage = $storage;
   $this->i18n = $i18n;
 }

 function setLanguage($language)
 {
   $this->storage->set('language', $language);

     $this->i18n->setLanguage($language);
 }

  // ...
•  La dépendance entre User et Storage et logique
  –  On ne peut pas utiliser le User sans Storage


•  Mais le lien entre User et I18n est moins évident
  –  On doit pouvoir utiliser la classe User sans I18n
class User
{
  protected $storage, $i18n;

 function __construct($storage, $i18n = null)
 {
   $this->storage = $storage;
   $this->i18n = $i18n;
 }

 function setLanguage($language)
 {
   $this->storage->set('language', $language);

     if (!is_null($this->i18n))
     {
       $this->i18n->setLanguage($language);
     }
 }
class User
{
  protected $storage;

    function __construct($storage)
    {
      $this->storage = $storage;
    }

    function setI18n($i18n)
    {
      $this->i18n = $i18n;
    }

    function setLanguage($language)
    {
      $this->storage->set('language', $language);

        if (!is_null($this->i18n))
        {
          $this->i18n->setLanguage($language);
        }
    }
}
L’Observateur
•  Le motif Observateur permet qu’un changement
   dans un objet permettent une mise à jour pour
   d’autres

•  C’est un moyen efficace de permettre un lien
   entre des classes sans avoir à les modifier

•  Mots-clés : hook, listener, event, subject, …
•  Utilisé par un nombre important de logiciels:
        –  Plugins (Wordpress, …)
        –  Bridges between applications
                •  As a master application:
                        –  Gallery2 (90% events / hooks for user / group create / update / delete, logout,
                           login, ..missing: configuration changes)
                        –  Xaraya (100% events / hooks for user / group create / update / delete,
                           configuration changes, logout, login, ..)
                        –  Wordpress (90% events / hooks for user / update / delete, rewrites, logout,
                           login, ..)
                        –  Drupal (80% maybe?)
                        –  Joomla (100% joomla 1.5; login, logout, create, update, delete user, block,
                           activation, system before and after start)Typo3 (50% maybe?, e.g. no unified create
                           user event / hook)
                •  As a slave application:
                        –  Gallery 2
                        –  Phorum.org
http://codex.gallery2.org/Gallery2:Embedding:Event-Based_Loose-Coupled_Integration
Implémentations en PHP
•  PEAR_Dispatcher
  –  http://pear.php.net/package/Event_Dispatcher


•  symfony implementation
  –  http://svn.symfony-project.com/branches/1.1/lib/event/
  –  http://www.symfony-project.org/book/1_1/17-Extending-Symfony
  –  Basé sur le Cocoa notification center
     •  Simple et puissant
     •  Decouplé desymfony
     •  Simple à utiliser
$i18n = new I18n();
$dispatcher = new sfEventDispatcher();
$listener = array($i18n, 'listenToChangeCultureEvent');
$dispatcher->connect('user.change_language', $listener);

$storage = new FileSessionStorage();
$user = new User($dispatcher, $storage);
class User
{
  protected $storage, $dispatcher;

 function __construct($dispatcher, $storage)
 {
   $this->dispatcher = $dispatcher;
   $this->storage = $storage;
 }

 function setLanguage($language)
 {
   $this->storage->set('language', $language);

     $event = new sfEvent(
        $this,
        'user.change_language',
        array('language' => $language)
     );

     $this->dispatcher->notify($event);
 }
Notifiers          Dispatcher             Listeners

                                                 I18n listens
1                                          to user.change_culture

       User notifies           Calls           I18n callback
2   user.change_culture    all listeners          is called
L’objet User ne connaît rien de l’objet I18n
L’objet I18n ne connaît rien de l’objet User

Il communique grâce à l’objet Dispatcher

N’importe quelle classe peut écouter l’événement
  ‘user.change_culture’ et agir en conséquence
Notifiers          Dispatcher             Listeners

                                                 I18n listens
1                                          to user.change_culture
                                              Your class listens
                                           to user.change_culture

       User notifies           Calls           I18n callback
2   user.change_culture    all listeners          is called
                                             Your class callback
                                                  is called
Notifiers          Dispatcher




   User notifies       Calls
user.change_culture   nothing



                                Peu d’impact
                                     de
                                performance
Dans cette exemple, l’implémentation est simple
 Pas d’interface à implémenter
  Pas besoin de créer une classe pour chaque
  événément
 Un événement est « juste » :
   un identifiant unique (user.change_culture)
   quelques conventions (noms des paramètres)
Avantages
  Simple à l’utilisation
  Facile d’ajouter des paramètres supplémentaires
  Très rapide
  Très facile d’ajouter un nouvel événement en
   notifiant un nouvel identifiant
Inconvénients
 Le contrat est assez lâche entre les participants à
   l’événement
Implémentation
function connect($name, $listener)
{
  if (!isset($this->listeners[$name]))
  {
    $this->listeners[$name] = array();
  }

    $this->listeners[$name][] = $listener;
}

function notify(sfEvent $event)
{
  if (!isset($this->listeners[$name]))
  {
    foreach ($this->listeners[$event->getName()] as $listener)
    {
      call_user_func($listener, $event);
    }
  }
}
•  3 types de notification
  –  Notify : tous les listeners sont appelés les uns après
     les autres (sans feedback possible)
     •  Logging, …
  –  Notify Until : tous les listeners sont appelés jusqu’à ce
     que l’un d’entre eux déclare avoir géré l’événement.
     Le listener peut alors retourner quelque chose
     •  Exceptions, Method not found in __call(), …
  –  Filter : chaque listener filtre une valeur. La valeur
     filtrée est alors retournée
     •  HTML content, parameters, …
Questions?

            Contact
       Fabien Potencier
 fabien.potencier@sensio.com



  http://www.sensiolabs.com/
http://www.symfony-project.org/

Mais conteúdo relacionado

Mais procurados

Programmation orientée objet en PHP 5
Programmation orientée objet en PHP 5Programmation orientée objet en PHP 5
Programmation orientée objet en PHP 5Kristen Le Liboux
 
OWF12/HTML 5 local storage , olivier thomas, cto at webtyss
OWF12/HTML 5 local storage , olivier thomas, cto at webtyssOWF12/HTML 5 local storage , olivier thomas, cto at webtyss
OWF12/HTML 5 local storage , olivier thomas, cto at webtyssParis Open Source Summit
 
Environnements, Sources de propriétés et Profils avec Spring 3.1
Environnements, Sources de propriétés et Profils avec Spring 3.1Environnements, Sources de propriétés et Profils avec Spring 3.1
Environnements, Sources de propriétés et Profils avec Spring 3.1Fabien Baligand
 
Développement sécurisé d'applications avec Zend Framework
Développement sécurisé d'applications avec Zend FrameworkDéveloppement sécurisé d'applications avec Zend Framework
Développement sécurisé d'applications avec Zend FrameworkMickael Perraud
 
Accès aux bases de données via jdbc
Accès aux bases de données via jdbcAccès aux bases de données via jdbc
Accès aux bases de données via jdbcRachid Lajouad
 
Trouvez la faille! - Confoo 2012
Trouvez la faille! - Confoo 2012Trouvez la faille! - Confoo 2012
Trouvez la faille! - Confoo 2012Antonio Fontes
 
Cours j query-id1575
Cours j query-id1575Cours j query-id1575
Cours j query-id1575kate2013
 
Jquery - introduction au langage
Jquery - introduction au langageJquery - introduction au langage
Jquery - introduction au langageStrasWeb
 
Php 2 - Approfondissement MySQL, PDO et MVC
Php 2 - Approfondissement MySQL, PDO et MVCPhp 2 - Approfondissement MySQL, PDO et MVC
Php 2 - Approfondissement MySQL, PDO et MVCPierre Faure
 
Php mysql cours
Php mysql coursPhp mysql cours
Php mysql courszan
 

Mais procurados (18)

Programmation orientée objet en PHP 5
Programmation orientée objet en PHP 5Programmation orientée objet en PHP 5
Programmation orientée objet en PHP 5
 
Jquery : les bases
Jquery : les basesJquery : les bases
Jquery : les bases
 
OWF12/HTML 5 local storage , olivier thomas, cto at webtyss
OWF12/HTML 5 local storage , olivier thomas, cto at webtyssOWF12/HTML 5 local storage , olivier thomas, cto at webtyss
OWF12/HTML 5 local storage , olivier thomas, cto at webtyss
 
Environnements, Sources de propriétés et Profils avec Spring 3.1
Environnements, Sources de propriétés et Profils avec Spring 3.1Environnements, Sources de propriétés et Profils avec Spring 3.1
Environnements, Sources de propriétés et Profils avec Spring 3.1
 
Développement sécurisé d'applications avec Zend Framework
Développement sécurisé d'applications avec Zend FrameworkDéveloppement sécurisé d'applications avec Zend Framework
Développement sécurisé d'applications avec Zend Framework
 
Accès aux bases de données via jdbc
Accès aux bases de données via jdbcAccès aux bases de données via jdbc
Accès aux bases de données via jdbc
 
Introduction a jQuery
Introduction a jQueryIntroduction a jQuery
Introduction a jQuery
 
JQuery
JQueryJQuery
JQuery
 
Trouvez la faille! - Confoo 2012
Trouvez la faille! - Confoo 2012Trouvez la faille! - Confoo 2012
Trouvez la faille! - Confoo 2012
 
Cours j query-id1575
Cours j query-id1575Cours j query-id1575
Cours j query-id1575
 
Jquery - introduction au langage
Jquery - introduction au langageJquery - introduction au langage
Jquery - introduction au langage
 
Apprenez le jQuery
Apprenez le jQueryApprenez le jQuery
Apprenez le jQuery
 
Jdbc
JdbcJdbc
Jdbc
 
Jdbc
JdbcJdbc
Jdbc
 
Php 2 - Approfondissement MySQL, PDO et MVC
Php 2 - Approfondissement MySQL, PDO et MVCPhp 2 - Approfondissement MySQL, PDO et MVC
Php 2 - Approfondissement MySQL, PDO et MVC
 
Les structures de données PHP5
Les structures de données PHP5Les structures de données PHP5
Les structures de données PHP5
 
Php mysql cours
Php mysql coursPhp mysql cours
Php mysql cours
 
22410 b 04
22410 b 0422410 b 04
22410 b 04
 

Destaque

Universal Sleeve's new product catalog (Bu Sleeves)
Universal Sleeve's new product catalog (Bu Sleeves)Universal Sleeve's new product catalog (Bu Sleeves)
Universal Sleeve's new product catalog (Bu Sleeves)Jose Giraldez
 
Marchés publics vs qualité
Marchés publics vs qualitéMarchés publics vs qualité
Marchés publics vs qualitéFEANTSA
 
FROST & SULLIVAN 'New Product Innovation Leadership Award' - Rapport en Français
FROST & SULLIVAN 'New Product Innovation Leadership Award' - Rapport en FrançaisFROST & SULLIVAN 'New Product Innovation Leadership Award' - Rapport en Français
FROST & SULLIVAN 'New Product Innovation Leadership Award' - Rapport en FrançaisAdvansolar
 
Uniform motion powerpoint presentation
Uniform motion powerpoint presentationUniform motion powerpoint presentation
Uniform motion powerpoint presentationAndy Richards
 

Destaque (6)

Universal Sleeve's new product catalog (Bu Sleeves)
Universal Sleeve's new product catalog (Bu Sleeves)Universal Sleeve's new product catalog (Bu Sleeves)
Universal Sleeve's new product catalog (Bu Sleeves)
 
Marchés publics vs qualité
Marchés publics vs qualitéMarchés publics vs qualité
Marchés publics vs qualité
 
Wine Brand Scan
Wine Brand ScanWine Brand Scan
Wine Brand Scan
 
FROST & SULLIVAN 'New Product Innovation Leadership Award' - Rapport en Français
FROST & SULLIVAN 'New Product Innovation Leadership Award' - Rapport en FrançaisFROST & SULLIVAN 'New Product Innovation Leadership Award' - Rapport en Français
FROST & SULLIVAN 'New Product Innovation Leadership Award' - Rapport en Français
 
Chap7 java net
Chap7 java netChap7 java net
Chap7 java net
 
Uniform motion powerpoint presentation
Uniform motion powerpoint presentationUniform motion powerpoint presentation
Uniform motion powerpoint presentation
 

Semelhante a Découpler votre code pour assurer la réutilisabilité et la maintenabilite (Forum PHP 2008)

Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !Engineor
 
201211 drupagora hostingdrupal
201211 drupagora hostingdrupal201211 drupagora hostingdrupal
201211 drupagora hostingdrupalOxalide
 
Concevoir, développer et sécuriser des micro-services avec Spring Boot
Concevoir, développer et sécuriser des micro-services avec Spring BootConcevoir, développer et sécuriser des micro-services avec Spring Boot
Concevoir, développer et sécuriser des micro-services avec Spring BootDNG Consulting
 
Workshop spring session 2 - La persistance au sein des applications Java
Workshop spring   session 2 - La persistance au sein des applications JavaWorkshop spring   session 2 - La persistance au sein des applications Java
Workshop spring session 2 - La persistance au sein des applications JavaAntoine Rey
 
PHP 5 pour les développeurs Java
PHP 5 pour les développeurs JavaPHP 5 pour les développeurs Java
PHP 5 pour les développeurs JavaMehdi EL KRARI
 
11. Autorisations.pptx
11. Autorisations.pptx11. Autorisations.pptx
11. Autorisations.pptxZinebJbilou
 
démonstration code source site web ecole.docx
démonstration code source site web ecole.docxdémonstration code source site web ecole.docx
démonstration code source site web ecole.docxVincentBweka
 
Bases de données sous Android.pdf
Bases de données sous Android.pdfBases de données sous Android.pdf
Bases de données sous Android.pdfRihabBENLAMINE
 
Créer une barre de progression grâce à PHP 5.4
Créer une barre de progression grâce à PHP 5.4Créer une barre de progression grâce à PHP 5.4
Créer une barre de progression grâce à PHP 5.4🏁 Pierre-Henry Soria 💡
 
ZendFramework2 - Présentation
ZendFramework2 - PrésentationZendFramework2 - Présentation
ZendFramework2 - Présentationjulien pauli
 
Draft - Developper Sur Elgg
Draft - Developper Sur ElggDraft - Developper Sur Elgg
Draft - Developper Sur ElggBrice Gaillard
 
Atelier WordPress: Création d&rsquo;extension WordPress
Atelier WordPress: Création d&rsquo;extension WordPressAtelier WordPress: Création d&rsquo;extension WordPress
Atelier WordPress: Création d&rsquo;extension WordPressIZZA Samir
 
Quelle place pour le framework Rails dans le développement d'application web
Quelle place pour le framework Rails dans le développement d'application webQuelle place pour le framework Rails dans le développement d'application web
Quelle place pour le framework Rails dans le développement d'application web5pidou
 

Semelhante a Découpler votre code pour assurer la réutilisabilité et la maintenabilite (Forum PHP 2008) (20)

Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !
 
201211 drupagora hostingdrupal
201211 drupagora hostingdrupal201211 drupagora hostingdrupal
201211 drupagora hostingdrupal
 
Concevoir, développer et sécuriser des micro-services avec Spring Boot
Concevoir, développer et sécuriser des micro-services avec Spring BootConcevoir, développer et sécuriser des micro-services avec Spring Boot
Concevoir, développer et sécuriser des micro-services avec Spring Boot
 
Workshop spring session 2 - La persistance au sein des applications Java
Workshop spring   session 2 - La persistance au sein des applications JavaWorkshop spring   session 2 - La persistance au sein des applications Java
Workshop spring session 2 - La persistance au sein des applications Java
 
Zf2 ce-qui-va-changer
Zf2 ce-qui-va-changerZf2 ce-qui-va-changer
Zf2 ce-qui-va-changer
 
PHP 5 pour les développeurs Java
PHP 5 pour les développeurs JavaPHP 5 pour les développeurs Java
PHP 5 pour les développeurs Java
 
11. Autorisations.pptx
11. Autorisations.pptx11. Autorisations.pptx
11. Autorisations.pptx
 
Javascript et JQuery
Javascript et JQueryJavascript et JQuery
Javascript et JQuery
 
Playing With PHP 5.3
Playing With PHP 5.3Playing With PHP 5.3
Playing With PHP 5.3
 
démonstration code source site web ecole.docx
démonstration code source site web ecole.docxdémonstration code source site web ecole.docx
démonstration code source site web ecole.docx
 
Client base de données en PHP5
Client base de données en PHP5Client base de données en PHP5
Client base de données en PHP5
 
Bases de données sous Android.pdf
Bases de données sous Android.pdfBases de données sous Android.pdf
Bases de données sous Android.pdf
 
Créer une barre de progression grâce à PHP 5.4
Créer une barre de progression grâce à PHP 5.4Créer une barre de progression grâce à PHP 5.4
Créer une barre de progression grâce à PHP 5.4
 
ZendFramework2 - Présentation
ZendFramework2 - PrésentationZendFramework2 - Présentation
ZendFramework2 - Présentation
 
De legacy à symfony
De legacy à symfonyDe legacy à symfony
De legacy à symfony
 
Draft - Developper Sur Elgg
Draft - Developper Sur ElggDraft - Developper Sur Elgg
Draft - Developper Sur Elgg
 
Rapport tp3 j2ee
Rapport tp3 j2eeRapport tp3 j2ee
Rapport tp3 j2ee
 
Atelier WordPress: Création d&rsquo;extension WordPress
Atelier WordPress: Création d&rsquo;extension WordPressAtelier WordPress: Création d&rsquo;extension WordPress
Atelier WordPress: Création d&rsquo;extension WordPress
 
Quelle place pour le framework Rails dans le développement d'application web
Quelle place pour le framework Rails dans le développement d'application webQuelle place pour le framework Rails dans le développement d'application web
Quelle place pour le framework Rails dans le développement d'application web
 
Php1
Php1Php1
Php1
 

Mais de Fabien Potencier

Dependency injection in PHP 5.3/5.4
Dependency injection in PHP 5.3/5.4Dependency injection in PHP 5.3/5.4
Dependency injection in PHP 5.3/5.4Fabien Potencier
 
Dependency injection-zendcon-2010
Dependency injection-zendcon-2010Dependency injection-zendcon-2010
Dependency injection-zendcon-2010Fabien Potencier
 
Design patterns revisited with PHP 5.3
Design patterns revisited with PHP 5.3Design patterns revisited with PHP 5.3
Design patterns revisited with PHP 5.3Fabien Potencier
 
The state of Symfony2 - SymfonyDay 2010
The state of Symfony2 - SymfonyDay 2010The state of Symfony2 - SymfonyDay 2010
The state of Symfony2 - SymfonyDay 2010Fabien Potencier
 
Dependency injection - phpday 2010
Dependency injection - phpday 2010Dependency injection - phpday 2010
Dependency injection - phpday 2010Fabien Potencier
 
Dependency Injection IPC 201
Dependency Injection IPC 201Dependency Injection IPC 201
Dependency Injection IPC 201Fabien Potencier
 
Caching on the Edge with Symfony2
Caching on the Edge with Symfony2Caching on the Edge with Symfony2
Caching on the Edge with Symfony2Fabien Potencier
 
Unit and Functional Testing with Symfony2
Unit and Functional Testing with Symfony2Unit and Functional Testing with Symfony2
Unit and Functional Testing with Symfony2Fabien Potencier
 
News of the Symfony2 World
News of the Symfony2 WorldNews of the Symfony2 World
News of the Symfony2 WorldFabien Potencier
 
Dependency Injection - ConFoo 2010
Dependency Injection - ConFoo 2010Dependency Injection - ConFoo 2010
Dependency Injection - ConFoo 2010Fabien Potencier
 

Mais de Fabien Potencier (20)

Varnish
VarnishVarnish
Varnish
 
Look beyond PHP
Look beyond PHPLook beyond PHP
Look beyond PHP
 
Dependency injection in PHP 5.3/5.4
Dependency injection in PHP 5.3/5.4Dependency injection in PHP 5.3/5.4
Dependency injection in PHP 5.3/5.4
 
Dependency injection-zendcon-2010
Dependency injection-zendcon-2010Dependency injection-zendcon-2010
Dependency injection-zendcon-2010
 
Caching on the Edge
Caching on the EdgeCaching on the Edge
Caching on the Edge
 
Design patterns revisited with PHP 5.3
Design patterns revisited with PHP 5.3Design patterns revisited with PHP 5.3
Design patterns revisited with PHP 5.3
 
The state of Symfony2 - SymfonyDay 2010
The state of Symfony2 - SymfonyDay 2010The state of Symfony2 - SymfonyDay 2010
The state of Symfony2 - SymfonyDay 2010
 
PhpBB meets Symfony2
PhpBB meets Symfony2PhpBB meets Symfony2
PhpBB meets Symfony2
 
Dependency injection - phpday 2010
Dependency injection - phpday 2010Dependency injection - phpday 2010
Dependency injection - phpday 2010
 
Symfony2 - WebExpo 2010
Symfony2 - WebExpo 2010Symfony2 - WebExpo 2010
Symfony2 - WebExpo 2010
 
Symfony2 - WebExpo 2010
Symfony2 - WebExpo 2010Symfony2 - WebExpo 2010
Symfony2 - WebExpo 2010
 
Symfony2 - OSIDays 2010
Symfony2 - OSIDays 2010Symfony2 - OSIDays 2010
Symfony2 - OSIDays 2010
 
Dependency Injection IPC 201
Dependency Injection IPC 201Dependency Injection IPC 201
Dependency Injection IPC 201
 
Caching on the Edge with Symfony2
Caching on the Edge with Symfony2Caching on the Edge with Symfony2
Caching on the Edge with Symfony2
 
Unit and Functional Testing with Symfony2
Unit and Functional Testing with Symfony2Unit and Functional Testing with Symfony2
Unit and Functional Testing with Symfony2
 
News of the Symfony2 World
News of the Symfony2 WorldNews of the Symfony2 World
News of the Symfony2 World
 
Dependency Injection - ConFoo 2010
Dependency Injection - ConFoo 2010Dependency Injection - ConFoo 2010
Dependency Injection - ConFoo 2010
 
Dependency Injection
Dependency InjectionDependency Injection
Dependency Injection
 
Symfony Components
Symfony ComponentsSymfony Components
Symfony Components
 
PHP 5.3 in practice
PHP 5.3 in practicePHP 5.3 in practice
PHP 5.3 in practice
 

Découpler votre code pour assurer la réutilisabilité et la maintenabilite (Forum PHP 2008)

  • 1. Découpler votre code pour assurer la réutilisabilité et la maintenabilité Fabien Potencier
  • 2. Fabien Potencier •  Créateur de Sensio –  Agence Web –  Depuis 1998 –  50 collaborateurs –  Spécialistes Open-Source –  Clients grands comptes / institutionnels •  Créateur et développeur principal de symfony –  Framework PHP
  • 3. Couplage ? Coupling is the degree to which each program module relies on each one of the other modules. http://en.wikipedia.org/wiki/Coupling_(computer_science)
  • 4. Couplage faible Avec un couplage faible, un changement dans un module ne nécessite pas un changement dans l’ implementation d’un autre module.
  • 5. Couplage faible Un module peut intéragir avec un autre à travers une interface stable
  • 7. Testabilité La possibilité d’instancier une classe dans un test prouve le niveau de couplage du code
  • 8. Readabilité On passe plus de temps à lire et à maintenir du code, qu’à en écrire
  • 9. Maintainabilité Un changement ici ne doit pas causer un problème là-bas
  • 10. Extensibilité Des petits modules indépendants facilite son extension
  • 12. Le monde du développement évolue vite… très vite Mais le but ultime de toutes ces évolutions reste le même
  • 13. Construire de meilleurs outils pour éviter de réinventer la roue Construire sur du code / bibliothèques existants Le spécialiser pour ses propres besoins … C’est pour ces raisons que l’Open-Source est toujours gagnant
  • 14. La construction d’outils meilleurs est possible parce que nous les basons sur des outils existants On apprend des projets existants On adopte leurs idées On les améliore C’est le processus naturel de l’évolution
  • 15. Réutilisabilité Configuration Personnalisation / Extension Documentation / Support
  • 17. Un exemple Web concret
  • 18. La plupart des applications doivent stocker les préférences des utilisateurs –  Langue de l’utilisateur –  S’il est authentifié ou non –  Ses droits –  …
  • 19. C’est possible avec un objet User – setLanguage(), getLanguage() – setAuthenticated(), isAuthenticated() – addCredential(), hasCredential() – ...
  • 20. La classe User doit trouver un moyen de stocker ces données entre les requêtes HTTP En PHP, on utilise des sessions
  • 21. class SessionStorage { function __construct($cookieName = 'PHP_SESS_ID') { session_name($cookieName); session_start(); } function set($key, $value) { $_SESSION[$key] = $value; } // ... }
  • 22. class User { protected $storage; function __construct() { $this->storage = new SessionStorage(); } function setLanguage($language) { $this->storage->set('language', $language); } // ... } $user = new User();
  • 23. Je veux changer le nom du cookie
  • 24. class User { Directement protected $storage; dans la classe User function __construct() { $this->storage = new SessionStorage('SESSION_ID'); } function setLanguage($language) { $this->storage->set('language', $language); } // ... } $user = new User();
  • 25. class User { protected $storage; Configuration globale ? function __construct() { $this->storage = new SessionStorage(STORAGE_SESSION_NAME); } } define('STORAGE_SESSION_NAME', 'SESSION_ID'); $user = new User();
  • 26. Configuration class User via le { constructeur protected $storage; function __construct($sessionName) { $this->storage = new SessionStorage($sessionName); } } $user = new User('SESSION_ID');
  • 27. Configuration avec un class User tableau { protected $storage; function __construct($storageOptions) { $this->storage = new SessionStorage($storageOptions['session_name']); $user = new User( array('session_name' => 'SESSION_ID') );
  • 28. Je veux changer le support de stockage des sessions Filesystem MySQL SQLite …
  • 29. Utilisation d’un class User registre global ? { protected $storage; function __construct() { $this->storage = Registry::get('session_storage'); } } $storage = new SessionStorage(); Registry::set('session_storage', $storage); $user = new User();
  • 30. La classe User dépend maintenant de la classe Registry
  • 31. Plutôt que de créer l’instance Storage dans la classe User On injecte la dépendance Storage dans l’objet User
  • 32. Arugment du class User constructeur ? { protected $storage; function __construct($storage) { $this->storage = $storage; } } $storage = new SessionStorage('SESSION_ID'); $user = new User($storage);
  • 33. Utilisation de différent support de stockage
  • 34. class User { protected $storage; Support de function __construct($storage) stockage { différent $this->storage = $storage; } } $storage = new MySQLSessionStorage('SESSION_ID'); $user = new User($storage);
  • 36. class User { protected $storage; function __construct($storage) Configuration { naturelle $this->storage = $storage; } } $storage = new MySQLSessionStorage('SESSION_ID'); $user = new User($storage);
  • 37. Utilisation de classes tierces (Interface / Adapter)
  • 38. class User { protected $storage; function __construct(ISessionStorage $storage) { $this->storage = $storage; } Ajout d’une } interface interface ISessionStorage { function get($key); function set($key, $value); }
  • 39. Remplacer l’objet Storage pour les tests (Mock)
  • 40. class User { protected $storage; function __construct(ISessionStorage $storage) { $this->storage = $storage; } } class SessionStorageForTests implements ISessionStorage { protected $data function set($key, $value) { Mock de la self::$data[$key] = $value; session } }
  • 41. Utilisation de différent supports de stockage Configuration naturelle Intégration de classes tierces Simplicité des tests Facile sans changement de la classe User
  • 42. C’est l’Injection de Dépendance Rien de plus
  • 43. « Dependency Injection is where components are given their dependencies through their constructors, methods, or directly into fields. » http://www.picocontainer.org/injection.html
  • 44. $storage = new SessionStorage(); // constructor injection $user = new User($storage); // setter injection $user = new User(); $user->setStorage($storage); // property injection $user = new User(); $user->storage = $storage;
  • 45. Un exemple un peu plus complexe
  • 46. $request = new WebRequest(); $response = new WebResponse(); $storage = new FileSessionStorage('SESSION_ID'); $user = new User($storage); $cache = new FileCache( array('dir' => dirname(__FILE__).'/cache') ); $routing = new Routing($cache);
  • 47. class Application { function __construct() { $this->request = new WebRequest(); $this->response = new WebResponse(); $storage = new FileSessionStorage('SESSION_ID'); $this->user = new User($storage); $cache = new FileCache( array('dir' => dirname(__FILE__).'/cache') ); $this->routing = new Routing($cache); } } $application = new Application();
  • 48. Retour à la case départ
  • 49. class Application { function __construct() { $request = new WebRequest(); $response = new WebResponse(); $storage = new FileSessionStorage('SESSION_ID'); $user = new User($storage); $cache = new FileCache( array('dir' => dirname(__FILE__).'/cache') ); $routing = new Routing($cache); } } $application = new Application();
  • 50. On a besoin d’un Container Description des objets et des relations Configuration Création
  • 51. Description d’un service $storageDef = new ServiceDefinition( 'FileSessionStorage' ); Nom de la classe
  • 52. $storageDef = new ServiceDefinition( 'FileSessionStorage', array('SESSION_ID') ); Arugments à passer au construteur
  • 53. $container = new ServiceContainer(array( 'storage' => $storageDef, )); Chaque objet a un idenfiant unique
  • 54. $userDef = new ServiceDefinition(‘User’, array(new ServiceReference('storage')) ); Référence à un autre object
  • 55. $storageDef = new ServiceDefinition( ‘FileSessionStorage’, array(‘SESSION_ID’) ); $userDef = new ServiceDefinition(‘User’, array(new ServiceReference(‘storage’)) ); $container = new ServiceContainer(array( 'storage' => $storageDef, 'user' => $userDef, ));
  • 56. $user = $container->getService('user'); Récupération de la configuration de l’objet user Le user nécessite un objet storage pour son constructeur Récupération de la configuration de l’objet storage Création de l’objet user en injectant le storage
  • 57. $user = $container->getService('user'); Est à peu près équivalent à $storage = new SessionStorage('SESSION_ID'); $user = new User($storage);
  • 58. Un container peu gérer n’importe quelle classe (POPO) Les objets ne savent pas qu’ils sont gérés par un container
  • 60. public function getService ($id) { $def = $this->definitions[$id]; $r = new ReflectionClass($def->getClass()); if (is_null($r->getConstructor())) { return $r->newInstance(); } $args = $def->getArguments(); $args = $this->evaluateArguments($args); return $r->newInstanceArgs($args); }
  • 61. public function evaluateArguments($arg) { if (is_array($arg)) { return array_map( array($this, 'evaluateArguments'), $arg ); } if ( is_object($arg) && $arg instanceof ServiceReference ) { return $this->getService($arg); } return $arg; }
  • 62. Vers une vraie implémentation
  • 63. Scope Quand on récupère un service : On veux un nouvel objet à chaque fois ? où Où le même ? $userDef->setGlobal(true); $feedReaderDef->setGlobal(false);
  • 64. Définition des services $container = new ServiceContainer(array( PHP 'storage' => new ServiceDefinition('FileSessionStorage'), 'user' => new ServiceDefinition('User', array(new ServiceReference('storage')) ), )); <service id="storage" class="FileSessionStorage" /> XML <service id="user" class="User"> <constructor_arg type="service" id="storage" /> </service>
  • 65. Configuration des services La configuration <parameter key="storage.class"> est découplée SQLiteSessionStorage de la définition </parameter> <parameter key="storage.session_name"> SESSION_ID </parameter> <service id="storage" class="%storage.class%"> <constructor_arg> %storage.session_name% </constructor_arg> </service>
  • 66. Configuration des services [storage] class = SQLiteSessionStorage session_name = SESSION_ID <service id="storage" class="%storage.class%"> <constructor_arg> %storage.session_name% </constructor_arg> </service>
  • 67. // Access parameters $sessionName = $container['storage.session_name']; // Access services $user = $container->user;
  • 68. Imlpémentations en PHP •  Crafty: http://phpcrafty.sourceforge.net/ •  Garden: http://garden.tigris.org/ spring •  Stubbles: http://www.stubbles.net/wiki/Docs/IOC Google Guice •  symfony 2 will have its dependency injection framework spring
  • 69. Rappelez-vous que la plupart du temps, l’utilisation de l’Injecteur de Dépendances ne nécessite pas l’utilisation d’un Container
  • 70. Vous pouvez commencer l’utilisation de l’Injection de Dépendance aujourd’hui
  • 71. En l’implémentant dans vos projets Où en utilisant des bibliothèques externes qui utilisent déjà ce mécanisme sans Container
  • 72. symfony Zend Framework ezComponents Doctrine Swift Mailer …
  • 73. Ajoutons un peu d’i18n
  • 74. class User { protected $storage, $i18n; function __construct($storage, $i18n) { $this->storage = $storage; $this->i18n = $i18n; } function setLanguage($language) { $this->storage->set('language', $language); $this->i18n->setLanguage($language); } // ...
  • 75. •  La dépendance entre User et Storage et logique –  On ne peut pas utiliser le User sans Storage •  Mais le lien entre User et I18n est moins évident –  On doit pouvoir utiliser la classe User sans I18n
  • 76. class User { protected $storage, $i18n; function __construct($storage, $i18n = null) { $this->storage = $storage; $this->i18n = $i18n; } function setLanguage($language) { $this->storage->set('language', $language); if (!is_null($this->i18n)) { $this->i18n->setLanguage($language); } }
  • 77. class User { protected $storage; function __construct($storage) { $this->storage = $storage; } function setI18n($i18n) { $this->i18n = $i18n; } function setLanguage($language) { $this->storage->set('language', $language); if (!is_null($this->i18n)) { $this->i18n->setLanguage($language); } } }
  • 79. •  Le motif Observateur permet qu’un changement dans un objet permettent une mise à jour pour d’autres •  C’est un moyen efficace de permettre un lien entre des classes sans avoir à les modifier •  Mots-clés : hook, listener, event, subject, …
  • 80. •  Utilisé par un nombre important de logiciels: –  Plugins (Wordpress, …) –  Bridges between applications •  As a master application: –  Gallery2 (90% events / hooks for user / group create / update / delete, logout, login, ..missing: configuration changes) –  Xaraya (100% events / hooks for user / group create / update / delete, configuration changes, logout, login, ..) –  Wordpress (90% events / hooks for user / update / delete, rewrites, logout, login, ..) –  Drupal (80% maybe?) –  Joomla (100% joomla 1.5; login, logout, create, update, delete user, block, activation, system before and after start)Typo3 (50% maybe?, e.g. no unified create user event / hook) •  As a slave application: –  Gallery 2 –  Phorum.org http://codex.gallery2.org/Gallery2:Embedding:Event-Based_Loose-Coupled_Integration
  • 81. Implémentations en PHP •  PEAR_Dispatcher –  http://pear.php.net/package/Event_Dispatcher •  symfony implementation –  http://svn.symfony-project.com/branches/1.1/lib/event/ –  http://www.symfony-project.org/book/1_1/17-Extending-Symfony –  Basé sur le Cocoa notification center •  Simple et puissant •  Decouplé desymfony •  Simple à utiliser
  • 82. $i18n = new I18n(); $dispatcher = new sfEventDispatcher(); $listener = array($i18n, 'listenToChangeCultureEvent'); $dispatcher->connect('user.change_language', $listener); $storage = new FileSessionStorage(); $user = new User($dispatcher, $storage);
  • 83. class User { protected $storage, $dispatcher; function __construct($dispatcher, $storage) { $this->dispatcher = $dispatcher; $this->storage = $storage; } function setLanguage($language) { $this->storage->set('language', $language); $event = new sfEvent( $this, 'user.change_language', array('language' => $language) ); $this->dispatcher->notify($event); }
  • 84. Notifiers Dispatcher Listeners I18n listens 1 to user.change_culture User notifies Calls I18n callback 2 user.change_culture all listeners is called
  • 85. L’objet User ne connaît rien de l’objet I18n L’objet I18n ne connaît rien de l’objet User Il communique grâce à l’objet Dispatcher N’importe quelle classe peut écouter l’événement ‘user.change_culture’ et agir en conséquence
  • 86. Notifiers Dispatcher Listeners I18n listens 1 to user.change_culture Your class listens to user.change_culture User notifies Calls I18n callback 2 user.change_culture all listeners is called Your class callback is called
  • 87. Notifiers Dispatcher User notifies Calls user.change_culture nothing Peu d’impact de performance
  • 88. Dans cette exemple, l’implémentation est simple Pas d’interface à implémenter Pas besoin de créer une classe pour chaque événément Un événement est « juste » : un identifiant unique (user.change_culture) quelques conventions (noms des paramètres)
  • 89. Avantages Simple à l’utilisation Facile d’ajouter des paramètres supplémentaires Très rapide Très facile d’ajouter un nouvel événement en notifiant un nouvel identifiant Inconvénients Le contrat est assez lâche entre les participants à l’événement
  • 91. function connect($name, $listener) { if (!isset($this->listeners[$name])) { $this->listeners[$name] = array(); } $this->listeners[$name][] = $listener; } function notify(sfEvent $event) { if (!isset($this->listeners[$name])) { foreach ($this->listeners[$event->getName()] as $listener) { call_user_func($listener, $event); } } }
  • 92. •  3 types de notification –  Notify : tous les listeners sont appelés les uns après les autres (sans feedback possible) •  Logging, … –  Notify Until : tous les listeners sont appelés jusqu’à ce que l’un d’entre eux déclare avoir géré l’événement. Le listener peut alors retourner quelque chose •  Exceptions, Method not found in __call(), … –  Filter : chaque listener filtre une valeur. La valeur filtrée est alors retournée •  HTML content, parameters, …
  • 93. Questions? Contact Fabien Potencier fabien.potencier@sensio.com http://www.sensiolabs.com/ http://www.symfony-project.org/