Archive pour la catégorie ‘Symfony’

Symfony Doctrine NestedSet getTree + useResultCache

Jeudi 17 juin 2010

Pour un besoin d’un site Internet développé sous Symfony, j’ai décidé d’utiliser le behavior NestedSet pour gérer des catégories d’un catalogue produit sous la forme d’un arbre.

Un des inconvénients des arbres avec Doctrine NestedSet, c’est le nombre important de requêtes exécutées pour récupérer la totalité de l’arbre. Je vais vous montrer comment j’ai mis en place le cache Doctrine (useResultCache) avec le behavior NestedSet.

Tout d’abord, dans config/ProjectConfiguration.class.php, vérifiez que vous avez bien activé le cache Doctrine:

public function configureDoctrine(Doctrine_Manager $manager)
{
  /* Initialisation du cache Doctrine APC */
  $cacheDriver = new Doctrine_Cache_Apc();
  $manager->setAttribute(Doctrine::ATTR_QUERY_CACHE, $cacheDriver);
  $manager->setAttribute(Doctrine::ATTR_QUERY_CACHE_LIFESPAN, sfConfig::get('app_cache_lifetime')); //mettre cette variable dans app.yml
  $manager->setAttribute(Doctrine::ATTR_RESULT_CACHE, $cacheDriver);
  $manager->setAttribute(Doctrine::ATTR_RESULT_CACHE_LIFESPAN, sfConfig::get('app_cache_lifetime'));
}

Ensuite au niveau de votre action ou component, pour récupérer la totalité de votre arbre avec le cache d’activé :

$this->categories = Doctrine::getTable('category')->getTree();
$q = Doctrine::getTable('category')->createQuery()->useResultCache();
$this->categories->setBaseQuery($q);
$this->categories = $this->categories->fetchTree();

Et enfin, au niveau du template, voici le code pour afficher correctement votre arbre sous la forme de menu par exemple :

<h2>Catégories</h2>
<ul>
  <?php foreach ($categories as $node) : ?>

    <?php if($node['level'] == 0) continue; ?> //ici on affiche pas la catégorie ROOT
    <?php if($node['level'] == '1'): ?>
    <li>
      <span><a href="#"><?php echo $node['label'] ?></a></span>
      <?php if($node->getNode()->hasChildren()): ?>
        <ul id="child_<?php echo $node['id']; ?>">
        <?php foreach($node->getNode()->getChildren() as $child): ?>
          <li><a href="<?php echo url_for('category', $child); ?>"><?php echo $child->getLabel(); ?></a></li>
        <?php endforeach; ?>
        </ul>
      <?php endif; ?>
    </li>
    <?php endif;?>

  <?php endforeach; ?>
</ul>

Voilà, votre arbre est maintenant mis en cache!

To be continued…

Symfony et les templates de mail dynamiques

Dimanche 24 janvier 2010

Je vais maintenant aborder un aspect important dans tout bon projet Web : L’envoi de mails.

Tout site internet a, dans 90% des cas (pour ma part) besoin d’envoyer des mails automatiques :
- confirmation d’inscription
- mot de passe oublié
- notifications pour messages reçus
- etc…

Pour mes projets, j’avais plusieurs besoins (ou envies ;-) )
- un jolie template de mail (c’est toujours agréable de recevoir un jolie mail)
- véhiculer l’information principale de ce mail
- apporter des informations dynamique pour donner vie au mail.

Avec Symfony 1.3 & 1.4, l’envoi de mail a été simplifié grâce à l’apparition de SfMailer. Mais, je voulais aller plus loin, en utilisant le même modèle Symfony -> actions/templates/partials/composants pour les contenus de mail.

Je vais vous présenter cela par un exemple concret : Une confirmation d’inscription.

1. Problématique

Suite à une inscription sur votre site Internet, vous désirez envoyer un email de remerciement accompagné des derniers membres inscris et la dernière news rédigée.

2. Le template de mail

Créer un fichier apps/frontend/templates/layout_mail.php avec dedans le contenu de votre template :

<html>
  <head>...</head>
  <body>
    <div class='header'> ... Votre haut de mail </div>
    <div class='content'> <?php echo $sf_content ?>  </div>
    <div class='footer'> ... Votre pied de mail </div>
  </body>
</html>

3. Centralisation des emails

L’idée est de centraliser dans un module tous les différents mails envoyés automatiquement depuis le site Internet. Créer un module mail :

symfony generate:module frontend mail

Editer le fichier actions.class.php de ce module pour rajouter l’action pour l’email d’inscription :

<?php
class mailActions extends sfActions
{
  public function preExecute()
  {
    //On précise ici que tous les mails utiliseront le layout que nous avons spécialement défini pour cela
    $this->setLayout('layout_mail');
  }
  public function executeInscription(sfWebRequest $request)
  {
    $this->member = Doctrine::getTable('member')->find($request->getParameter('id'));
  }
}

4. Dynamisation des mails grâce aux components

Créer le fichier components.class.php dans le module mail :

<?php

class mailComponents extends sfComponents
{
  public function executeLastMembers()
  {
    $this->members = Doctrine::getTable('member')->getLast();
  }
  public function executeLastNews()
  {
    $this->news = Doctrine::getTable('news')->getLast();
  }
}

Créer ensuite les deux templates pour ces components : _lastMembers.php et _lastNews.php avec dedans l’affichage des derniers membres et de la dernière news comme vous le souhaitez.

5. Le template pour le mail d’inscription

Créer le fichier inscriptionSuccess.php dans le dossier templates du module mail :

<div>
 <?php echo $member->getFullName() ?>, nous vous remercions pour votre inscription sur www.iw2.fr.
</div>

<!-- Nous incluons ici les deux components pour afficher les derniers membres et la dernière news -->

<?php include_component('mail', 'lastMembers')?>

<?php include_component('mail', 'lastNews')?>

6. Une classe mailer pour l’envoi des mails

Cette classe se compose de deux méthodes: une pour centraliser les sujets de mails et l’autre pour l’envoi. Créer un fichier mailer.class.php dans le dossier lib.

class mailer
{
  static public function getSubject($action)
  {
    $subject = array();
    $subject['inscription'] = "Confirmation de votre inscription";
    //$subject['nom_de_l'action_du_module_mail'] = "Le sujet de ce mail";
    return $subject[$action];
  }

  static public function send ($dest, $action)
  {
    //En premier paramètre nous avons l'adresse mail du destinataire ou un tableau d'emails. En second paramètre nous avons le nom de l'action du module mail

    //On initialise l'objet mailer avec le destinataire et le sujet.
    $mailer = sfContext::getInstance()->getMailer()->compose(
              null,
              $dest,
              mailer::getSubject($action)
    );
    //On initialise le FROM avec des valeurs définie dans le app.yml par exemple
    $mailer->setFrom(sfConfig::get('app_mailer_from'), sfConfig::get('app_mailer_name'));
    //On précise un rendu de type html
    $mailer->setContentType("text/html");
    //On rajoute le contenu du mail en récupérant le rendu de l'action du module mail
    $mailer->setBody(sfContext::getInstance()->getController()->getPresentationFor('mail', $action));
    //On envoi le mail
    sfContext::getInstance()->getMailer()->send($mailer);
  }
}

Penser à faire un symfony cc à ce niveau pour prendre en compte cette nouvelle classe.

7. Au niveau de l’action d’inscription

Dans votre action qui enregistre l’inscription, exemple :

public function executeCreate(sfWebRequest $request)
{
  $this->form = new memberForm();
  if( $request->isMethod('post') )
  {
    $this->form->bind($request->getParameter( $this->form->getName() ), $request->getFiles($this->form->getName()));
    if($this->form->isValid()) {
      $member = $this->form->save();
      //On passe un paramètre à l'objet Request pour le récupérer au niveau de l'action du module mail
      $this->getRequest()->setParameter('id', $member->getId());
      //On envoi le mail
      mailer::send($pro->getEmail(), 'inscription');

      $this->redirect('page/remerciement');
    }
  }
}

8. Exemple de rendu

mail-keldeco

9. Conclusion

N’ayant rien trouvé sur internet pour répondre à mes besoins en matière d’envoi de mail « dynamique », j’ai décidé de partager avec vous ma solution. Je conçois que celle-ci ne répond peut être pas à tous les critères de BEST PRACTICES Symfony pour le moment, mais c’est une première ébauche qui remplie 100% son rôle. N’hésitez pas si vous avez des questions ou des commentaires.

To be continued…


Symfony – Forcer le téléchargement

Mercredi 28 octobre 2009

Voici un petit article rapide pour apporter une solution à cette question : « Comment forcer le téléchargement d’un fichier avec Symfony ».

symfony-logo

Je devais mettre en place aujourd’hui cette fonctionnalité pour un site et plus précisement pour une messagerie avec pièce jointe.

Après quelques essais personnels qui ont été un échec pour forcer le download de mon fichier, j’ai tenté de trouver des réponses sur le web. Bien évidement et comme toujours, plusieurs personnes avaient le même problème que moi ;-) Mais les réponses proposées n’étaient pas à la hauteur de mes espérances… cela ne fonctionnait toujours pas.

Et finalement, je suis tombé sur ce site : LoaderTool

Ils mettent à disposition une classe PHP pour symfony permettant de forcer le téléchargement très simplement et efficacement.

Il suffit de copier cette classe loadertool-class.php dans un de vos dossier dans le répertoire lib de votre projet Symfony, et ensuite créé une action de téléchargement sur un de vos modules :

public function executeDownload(sfWebRequest $request)
{
  //Dans mon cas, je récupère ici mon objet message.
  $this->forward404Unless($message = Doctrine::getTable('messenger')->find($request->getParameter('id'));

  //J'initialise la variable $file avec le chemin vers mon fichier
  $file = sfConfig::get('sf_upload_dir').'/messenger_pj/'.$message->getAttachment();

  //Je lance le téléchargement avec LoaderTool
  LoaderTool::downloadContent($file);
  exit();
}

Rien de plus simple. Il existe d’autres options pour cette classe que je vous laisse découvrir.

To be continued…

Symfony unsetAllExcept + i18n = unsetAllExceptI18n

Mardi 15 septembre 2009

Suite au commentaire de eddie sur l’article « Symfony – Formulaire en plusieurs étapes » voici un exemple d’un formulaire multilangue en plusieurs étapes.

symfony-logo

Petit rappel du contexte: symfony 1.2 + Doctrine

Dans le fichier schema.yml

Projet1Exemple:
  tableName: projet1_exemple
  actAs:
    I18n:
      fields: [titre, description, mots]
  columns:
    id:
      type: integer(4)
      primary: true
      unsigned: true
      notnull: true
      autoincrement: true
    fichier:
      type: string(255)
      unique: true
      notnull: true
    type:
      type: enum
      values: ['image', 'video', 'audio', 'document']
      notnull: true
      default: image
    titre:
      type: string(255)
      description:
      type: string(4000)
    mots:
      type: string(4000)

Nous avons donc deux champs communs à toutes les langues : fichier et type. Et trois champs multilangues : titre, description mots.

L’idée consisterait à mettre dans une première étape uniquement les champs : fichier (commun), titre (multilangue) et description (multilangue).

Dans le fichier actions.class.php

public function executeIndex(sfWebRequest $request) {
  $this->form = new Projet1ExempleForm()
}

Dans le fichier /lib/form/doctrine/BaseFormDoctrine.class.php, ajouter la fonction suivante :

public function unsetAllExceptI18n($keepFields = array(), $cultures = array(), $keepFieldsI18n = array()) {
  $keepFields[] = $cultures;
  foreach ($this as $key => $value)
    $baseFields[] = $key;
  $temp = array_diff($baseFields, $keepFields);
  foreach ($temp as $unsetField)
    unset($this[$unsetField]);
  $embeds = $this->getEmbeddedForms();
  foreach ($cultures as $culture)
    $formsI18n[$culture] = $embeds[$culture];
  foreach ($formsI18n as $culture => $formI18n)
  {
    foreach ($formI18n as $key => $value)
      $baseFieldsI18n[] = $key;
    $temp = array_diff($baseFieldsI18n, $keepFieldsI18n);
    foreach ($temp as $unsetField)
      unset($formI18n[$unsetField]);
    unset($this[$culture]);
    $this->embedForm($culture, $formI18n);
  }
}

Explication des paramètres :

  • -  »$keepFieds » : un tableau des champs ‘commun’ que vous souhaitez « garder ».
  • -  »$cultures » : un tableau des différentes cultures que vous voulez utiliser.
  • -  »$keepFieldsI18n » : un tableau des champs ‘multilangues’ que vous souhaitez « garder ».

Et enfin dans le fichier /lib/form/doctrine/Projet1ExempleForm.class.php

class Projet1ExempleForm extends BaseProjet1ExempleForm {
  public function configure() {
    //Introduire les formulaires multilangues dans un premier temps
    $this->embedI18n(array('fr'));
    $this->embedI18n(array('en'));
    //Garder uniquement les champs dont vous avez besoin
    $this->unsetAllExceptI18n(array('fichier'), array('fr', 'en'), array('titre', 'description'));
  }
}

Et enfin dans votre template

//Soit tout simplement
<?php echo $form;?>

//ou
<?php echo $form['fichier']->renderLabel()?>
<?php echo $form['fichier']->render()?>
<?php echo $form['fr']['description']->renderLabel()?>
<?php echo $form['fr']['description']->render()?>
//...

Voici un aperçu du résultat : Capture_d_ecran_2009-09-15_a_19.25.01

Vous pouvez ensuite continuer de la même manière pour les prochaines étapes de votre formulaire.

N’hésitez pas à me solliciter pour plus d’explications.

To be continued…

Symfony – Formulaire en plusieurs étapes

Vendredi 28 août 2009

Comment mettre en place un formulaire en plusieurs étapes avec Symfony 1.2 et Doctrine? Je vais tenter de vous expliquer cela par un exemple concret.

symfony-logo

Nous allons réaliser une inscription de membre en plusieurs étapes:

  • - Etape 1 : Identification  »(pseudo, mail, password) »
  • - Etape 2 : Informations complémentaires  »(nom, prenom, adresse, cp, ville) »

Dans le fichier /config/doctrine/schema.yml :

membre:
  columns:
    id: { type: integer(4), primary: true, notnull: true, autoincrement: true }
    pseudo: { type: string(255) }
    mail: { type: string(255) }
    password: { type: string(255) }
    nom: { type: string(255) }
    prenom: { type: string(255) }
    adresse: { type: string(255) }
    cp: { type: string(255) }
    ville: { type: string(255) }

Lancer ensuite la commande :

symfony doctrine:build-all

Editer le fichier /lib/form/doctrine/BaseFormDoctrine.class.php et ajouter cette fonction :

public function unsetAllExcept ($keepFields = array()) {
  foreach ($this as $key => $value)
    $baseFields[] = $key;
    $temp = array_diff($baseFields, $keepFields);
    foreach ($temp as $unsetField)
      unset($this[$unsetField]);
}

Nous allons maintenant créer deux formulaires ‘membre’ pour les deux étapes du formulaire.

Créer le fichier /lib/form/doctrine/membreEtape1Form.class.php

class membreEtape1Form extends BasemembreForm {
  public function configure () {
    $this->unsetAllExcept(array( 'pseudo', 'mail', 'password' ) );
  }
}

Créer maintenant le fichier /lib/form/doctrine/membreEtape2Form.class.php

class membreEtape2Form extends BasemembreForm {
  public function configure ()   {
    $this->unsetAllExcept(array( 'nom', 'prenom', 'adresse', 'cp', 'ville' ) );
  }
}

Lancer ensuite la commande pour vider le cache :

symfony cc

Vous pouvez maintenant initialiser vos formulaires dans actions.class.php

public function executeInscriptionEtape1(sfWebRequest $request) {
  $this->form = new membreEtape1Form();
}
public function executeInscriptionEtape2(sfWebRequest $request) {
  $this->form = new membreEtape2Form();
}

Vous pouvez dès à présent vous inspirer de cette base pour vos propres besoins.

N’hésitez pas à me contacter par commentaire pour plus d’explications.

To be continued…

Framework Symfony

Mardi 4 août 2009

Présentation

Symfony est un framework PHP 5 qui permet d’accélérer les temps de développement d’applications Web. Il permet d’avoir un site robuste, évolutif, et sécurisé, garantissant la pérennité de votre application dans le temps.

symfony-logo

Spécifications

Voici une liste non exhaustive de ce que propose le framework Symfony :

  • - Model MVC
  • - Générateur de back-office
  • - Internationalisation
  • - ORM (Mapping objet-relationnel) embarqué : Doctrine ou Propel.
  • - Création et utilisation de plugins
  • - Système de cache
  • - Moteur d’URL rewriting performant
  • - Fichiers de configuration (Langage YAML)

Pourquoi le choix de ce framework?

Je travaille actuellement dans une agence Web qui disposait jusqu’à présent de son propre framework maison. Actuellement en pleine expansion, nous avons pris la décision de migrer vers le framework Symfony pour plusieurs raisons:

  • - Accélérer nos temps de développement.
  • - DRY (Don’t Repeat Yourself) : Ne pas réinventer la roue.
  • - Capitaliser notre expérience en exportant certaines briques de code sous forme de Plugins pour d’autres projets.
  • - Se concentrer plus sur l’aspect métier des applications à développer.
  • - Recruter plus facilement des développeurs utilisant ou connaissant Symfony plutôt que perdre du temps pour les former sur notre framework maison.

Prise en main de Symfony

Je développe depuis quelques mois sous Symfony, et le chemin n’a pas toujours été facile… Je découvre et j’apprends tous les jours avec de framework qui ne cessera jamais de m’étonner. Voici quelques conseils à ceux qui souhaiterais se lancer dans ce framework :

  • - Suivre le tutoriel Jobeet présent sur le site de Symfony (je vous conseille l’ORM Doctrine pour la simple et bonne raison qu’il sera en standard dans les nouvelles versions de Symfony). Tutoriel pour démarrer avec Symfony
  • - Pour le développement de vos applications, je vous conseille fortement l’utilisation d’un outil de débugger tel que xdebug ou zend debugger, permettant de comprendre la logique de Symfony notamment pour tout ce qui concerne la soumission des formulaires.
  • - Et enfin n’hésitez pas à faire appel à la communauté Symfony très active : Groupe Google Symfony et également Canal IRC Fr

Liens utiles

Site officiel Symfony
Symfony sur Wikipedia
Tutoriel pour démarrer avec Symfony

Centralisation des réalisations faites avec le framework Symfony

to be continued…