Le blog de Noël GUILBERT

Aller au contenu Aller au menu

mardi, juin 30 2009

Utiliser l'injection de dépendance dans un projet Symfony

Fabien à publié aujourd'hui un nouveau composant Symfony : le Dependency Injection Container. J'en profite donc pour publier un petit billet sur l'utilisation de ce composant dans un projet Symfony. Je pars du principe que vous avez déjà un projet Symfony installé, et si ce n'est pas le cas, commencez par la page d'installation.

Dans ce billet, je vais reprendre directement les exemples présents dans la http://components.symfony-project.org/dependency-injection/documentation, à savoir créer un service permettant d'envoyer un mail.

Nous allons donc avoir besoin de deux librairies, à savoir:

  • Dependency injection Container bien évidemment
  • Zend Mail

Prérequis

Pour faire les choses rapidement, on va juste faire un export des librairies dans le repertoire lib/vendor de votre application. Si vous utilisez déjà Subversion avec votre projet, la meilleure façon sera d'ajouter ces deux repository en externals.

$> svn export http://svn.symfony-project.com/components/dependency_injection/trunk/ lib/vendor/dependency_injection
$> svn export http://framework.zend.com/svn/framework/standard/trunk/library/Zend lib/vendor/Zend

Création du Service Container

Tout d'abord, il faut initialiser l'autoloading pour pouvoir utiliser le composant. Ajoutez ces deux lignes dans le fichier

<?php

require_once dirname(__FILE__).'/../lib/vendor/dependency_injection/lib/sfServiceContainerAutoloader.php';
sfServiceContainerAutoloader::register();

Toujours dans ce fichier, nous allons ajouter une méthode initializeServiceContainer() pour initialiser le conteneur de services:

<?php

class ProjectConfiguration extends sfProjectConfiguration
{
  protected
    $serviceContainer;
 
  /* ... */

  protected function initializeServiceContainer()
  {
    $this->serviceContainer = new sfServiceContainerBuilder();
    $loader = new sfServiceContainerLoaderFileYaml($this->serviceContainer);

    if ($this instanceof sfApplicationConfiguration && file_exists($file = sfConfig::get('sf_config_dir').'/container_'.$this->environment.'.yml'))
    {
       $loader->load($file);
    }
    else
    {
      $loader->load(sfConfig::get('sf_config_dir').'/container.yml');
    }
  }

Le composant nous offre la possibilité de charger la configuration en XML ou YAML à travers les classes sfServiceContainerLoaderFileXml ou sfServiceContainerLoaderFileYaml. Dans cet exemple, nous utilisons le format YAML, pour rester cohérent avec les autres fichiers de configuration de Symfony.

La classe sfServiceContainerBuilder sera notre conteneur de services. C'est lui qui sera chargé de d'instancier et de configurer les différents services. Pour charger la configuration, nous demandons ensuite au loader de charger les fichier Yaml pour l'environnement actuel si il y en a un, ou la configuration par défaut le cas échéant (par exemple en mode cli).

Le fichier container.yml est exactement le même que celui de la documentation:

parameters:
  mailer.username: foo
  mailer.password: bar
  mailer.class:    Zend_Mail
 
services:
  mail.transport:
    class:     Zend_Mail_Transport_Smtp
    arguments: [smtp.gmail.com, { auth: login, username: %mailer.username%, password: %mailer.password%, ssl: ssl, port: 465 }]
    shared:    false
  mailer:
    class: %mailer.class%
    calls:
      - [setDefaultTransport, [@mail.transport]]

Ces fichiers ne font rien de plus qu'importer le fichier de configuration par défaut.

C'est à peut prêt tout ce que nous avons besoin pour utiliser l'injection de dépendance. Nous avons juste besoin d'initialiser automatiquement le service container, et d'avoir une méthode permettant d'y accéder.

Dans votre fichier config/ProjectConfiguration.class.php, nous ajoutons une méthode getServiceContainer(), et nous ajoutons l'appel à la méthode initializeServiceContainer() dans la méthode setup() :


<?php

class ProjectConfiguration extends sfProjectConfiguration
{
  /* ... */
  public function setup()
  {
    /* ... */

    $this->initializeServiceContainer();
  }

  public function getServiceContainer()
  {
    return $this->serviceContainer;
  }

/* ... */
}

Pour l'utiliser, c'est très simple. Si vous utiliser le service mail dans une action, c'est désormais assez simple:

<?php

public function executeSendMail(sfWebRequest $request)
{
  $this->mailer = sfApplicationConfiguration::getActive()->getServiceContainer()->mailer;
  $this->mailer->
    setBodyText('This is the text of the mail.')->
    setFrom('somebody@example.com', 'Some Sender')->
    addTo('somebody_else@example.com', 'Some Recipient')->
    setSubject('TestSubject')->
    send();
}

Oui, et alors?

C'est un petit peu ce que je me suis dis la 1ère fois que j'ai entendu parler d'injection de dépendance. Le gros avantage, c'est que désormais on va pouvoir configurer notre service en fonction de chaque environnement. Ça a un gros intérêt lors de l'écriture de tests unitaire par exemple, parce que vous ne voulez peut-être pas recevoir un mail à chaque fois que les tests sont lancés. Pour les tests, il va donc falloir désactiver l'envoi de mail. Sans l'injection de dépendance, il faudrait tester si on est en environnement de test directement dans l'action. Maintenant, il suffira de modifier la configuration, sans avoir à modifier le code.

Customiser la configuration pour l'environnement de test

En général, lors des tests, on écrit des classes qui simulent le comportement de la classe utilisées par l'application (les "Mocks"). C'est assez facile à mettre en place pour les tests unitaires, mais pas toujours évidents lors des tests fonctionnels. Ici, c'est très simple, il suffit d'ajouter un fichier de configuration pour l'environnement de test (config/container_test.yml) :

parameters:
  mailer.class:    Mail_Mock

services:
  mailer: 
    class: %mailer.class%

La classe Mail_Mock ici est très simpliste, elle reprend juste les méthodes utilisés dans notre action, et ne fait aucun traitement :

lib/test/Mail_Mock.class.php

<?php

class Mail_Mock
{
  public function setFrom()
  {
    return $this;
  }

  public function addTo()
  {
    return $this;
  }

  public function setBodyText()
  {
    return $this;
  }

  public function setSubject()
  {
    return $this;
  }

  public function send()
  {
    return true;
  }
}

Conclusion

Ce composant est l'une des pièces maitresse de Symfony 2, et fait entrer le framework dans la cour des grands comme Spring. L'autre gros avantage, c'est qu'il est complétement découplé de Symfony, et donc réutilisable sans avoir besoin du framework.

dimanche, mars 22 2009

symfony et sfForms : ajouter une étoile pour les champs obligatoires

Il est très courant sur le web d'avoir les champs obligatoires d'un formulaire marqués d'une étoile. Pourtant, avec symfony et les sfForm, ajouter ce caractère n'est pas si évident qu'il en à l'air.

La difficulté s'explique par le découplage total entre les principaux éléments d'un formulaire, à savoir les widgets et les validateurs. Ainsi, un widget n'as pas connaissance des validateurs qui lui sont attachés, et vice-versa. Pourtant, il est indispensable de savoir si un validateur à l'option "required". Pour pouvoir ajouter cette fameuse étoile, il va donc falloir trouver une solution pour que les deux éléments puissent interagir.

La semaine dernière, je suis tombé sur cet article, qui proposait une solution pour répondre à ce problème. Cette dernière me gène un peu car elle redéfinie directement le label en lui ajoutant des informations. Or, l'idéal serait d'ajouter ce caractère uniquement au moment du rendu : en effet, ce n'est que de l'affichage, donc du coté Vue du MVC.

Avec les sfForm, la Vue est caractérisée par la classe sfWidgetFormSchemaFormatter. Or cette classe a uniquement connaissance des widgets, il va donc falloir détourner légèrement son fonctionnement pour lui permettre d'avoir accès aux validateurs. Enfin, il va falloir surcharger la méthode generateLabelName(), dont le fonctionnement parle de lui même.

Sans plus attendre, voici donc notre nouveau formatter :

<?php

class sfWidgetFormSchemaFormatterRequiredFields extends sfWidgetFormSchemaFormatter
{
  protected
    validatorSchema ;

  /**
    * This constructor allow us to pass a sfValidatorSchema object to our formatter
    *
    */

  public function __construct(sfWidgetFormSchema $widgetSchema, sfValidatorSchema $validatorSchema)
  {
    parent::__construct($widgetSchema);

    $this->validatorSchema = $validatorSchema;
  }

  public function generateLabelName($name)
  {
    $label = parent::generateLabelName($name);
    // Is the field required ? If so we add a star to the label name
    $label .= $this->validatorSchema[$name]->getOption('required') ? ' <span class="required">*</span>' : '';

    return $label;
  }
}

Ensuite, il suffit de d'utiliser ce formatter à la place de celui par défaut. Pour cela, il suffit d'ajouter ces deux lignes dans la méthode configure() de votre formulaire:

$this->widgetSchema->addFormFormatter('RequiredFields', new sfWidgetFormSchemaFormatterRequiredFields($this->widgetSchema, $this->validatorSchema));
$this->widgetSchema->setFormFormatterName('RequiredFields');

Vous pouvez aussi surcharger la méthode setup() de la class BaseForm(Propel|Doctrine) pour ajouter ce comportement à tous vos formulaires.

Conclusion

Le découplage important des différents composants des sfForm rends ce genre de tache à priori simple plutôt compliquée, et cette solution nécessite une assez bonne connaissance du fonctionnement interne de ce framework. Mais en contrepartie, ça nous permet de gérer des choses plus complexes assez facilement.

mardi, mars 17 2009

Un formulaire d'inscription à CampaignMonitor avec symfony

Le but de ce post est de réaliser en quelques étapes un formulaire permettant d'inscrire un utilisateur à une liste de diffusion.

Pour ce faire, nous allons utiliser symfony, et CampaignMonitor. Ce dernier est outil d'emailing qui offre une API assez complète, et un portage en PHP pour faciliter son utilisation.

Pour commencer, vous aurez besoin d'un projet symfony (1.1 ou 1.2), et d'un compte campaign monitor (l'inscription est gratuite), ainsi que de la classe CampaignMonitor disponible ici.

Si vous venez tout juste de créer votre compte chez campaign monitor, vous devez créer un client et une nouvelle liste de diffusion avant de continuer.

Le formulaire d'inscription

Dans lib/form, créez un fichier CampaignMonitorSubscriptionForm.class.php, et ajouter le code ci-dessous:

class CampaignMonitorSubscriptionForm extends sfForm
{
  protected
    $campaignMonitor = null;
 
  public function setup()
  {
     $this->setOption('api_key', '1234567890ABCDEF');
     $this->setOption('client_id', '1234566789');
     $this->setOption('list_id', '123456789');

     $this->setWidgets(array(
       'email' => new sfWidgetFormInput()
     ));

     $this->setValidators(array(
       'email' => new sfValidatorEmail()
     ));

     $this->widgetSchema->setNameFormat('newsletter[%s]');
  }

  public function getCampaignMonitorInstance()
  {
    if (is_null($this->campaignMonitor))
    {
      $this->campaignMonitor = new CampaignMonitor($this->getOption('api_key'), $this->getOption('client_id'),  null, $this->getOption('list_id'));
    }

    return $this->campaignMonitor;
  }

  public function save()
  {
    $cm = $this->getCampaignMonitorInstance();

    if (!$cm->subscriberAddAndResubscribe($values['email'], null, $this->getOption('list_id')))
    {
      throw new Exception('An error occurred during the newsletter subscription');
    }
  }
}

Implémentation du module newsletter

Tout d'abord, il nous faut un module newsletter:

$> php symfony generate:module frontend newsletter

Il nous faut ensuite créer une action pour permettre l'affichage du formulaire et l'inscription de l'utilisateur.

Maintenant, utilisons la méthode executeIndex() de notre contrôleur:

class newsletterActions extends sfActions
{
 /**
  * Executes index action
  *
  * @param sfRequest $request A request object
  */

  public function executeIndex(sfWebRequest $request)
  {
    $this->form = new CampaignMonitorSubscriptionForm();

    $this->processForm($request);
  }

  protected function processForm($request)
  {
    if ($request->isMethod('post'))
    {
      $this->form->bind($request->getParameter('newsletter'));

      if ($this->form->isValid())
      {
        $this->form->save();
        $this->getUser()->setFlash('notice', 'Your registration has been accepted');

        $this->redirect('newsletter/index');
      }
    }
  }
}

Et enfin, ajoutons l'affichage du formulaire dans le template indexSuccess.php:

<?php if ($sf_user->hasFlash('notice')): ?>
  <h1><?php echo $sf_user->getFlash('notice') ?></h1>
<?php else: ?>
  <?php echo $form->renderFormTag(url_for('newsletter/index')) ?>
    <table>
      <?php echo $form ?>
      <tr><td><input type="submit" /></td></tr>
    </table>
  </form>
<?php endif ?>

En conclusion

On pourrait aller plus loin dans cet exemple en prenant en compte les champs personnalisable de CampaignMonitor. Cela est assez facile à implémenter, je vous laisse donc regarder du coté de l'API de CampaignMonitor. Néanmoins, grâce à symfony et aux sfForm, nous avons facilement réalisé un formulaire d'inscription à une liste de diffusion.