Un blog avec Symfony (8/)

Administration des commentaires

On génère le module d'administration :

$ symfony init-module back blogcomment  

Nous ajoutons le nombre de commentaires dans la liste des billets et dans le filtre.
Dans le fichier back/modules/post/config/generator.yml :

generator:
  class:              sfPropelAdminGenerator
  param:
    ...
    fields:
      ...
      number_of_comments:   { name: Nb Comment. }
   
    list:
      ...
      display:        [=title, category_name, excerpt, created_at, number_of_comments]
      ...
      filters:        [title, blog_category_id, created_at, _number_of_comments]

Par défaut, le champ d'un filtre sur les nombres est de type input.
Nous allons utiliser un partial afin de pouvoir créer 2 listes déroulantes (min et max) pour filtrer les commentaires. C'est pourquoi, au niveau de "filters" nous avons écrit _number_of_comments et non pas number_of_comments.
On crée un partial _number_of_comments.php dans back/modules/post/templates :

<?php
// nouveau critère de sélection
$c = new Criteria();
// selection uniquement du champ number_of_comments
$c->addSelectColumn(PostPeer::NUMBER_OF_COMMENTS);
// trie ascendant
$c->addAscendingOrderByColumn(PostPeer::NUMBER_OF_COMMENTS);
// execution de la requête
$rs = PostPeer::doSelectRS($c);

// génération d'un tableau avec les valeurs récupérées
while($rs->next())
  $vals[$rs->getInt(1)] = $rs->getInt(1); 

echo 'entre ';
// création de la balise <select>...</select>
echo select_tag(
        // nom de la balise html
        'filters[number_of_comments_min]'
        // la liste des options du <select>...</select>
        ,options_for_select(
                // les données de la base de données
                $vals
                // la valeur par défaut
                ,isset($filters['number_of_comments_min'])?$filters['number_of_comments_min']:''
                // options supplémentaires ; ici pour ajouter une valeur "vide" dans la la liste
                ,array('include_blank' => true)
                )
        );
       
echo ' et ';
echo select_tag(
        'filters[number_of_comments_max]'
        ,options_for_select(
                $vals
                ,isset($filters['number_of_comments_max'])?$filters['number_of_comments_max']:''
                ,array('include_blank' => true)
                )
        );                
?>

Les critères des filtres sont ajoutés à la requête SQL de sélection des données à l'aide de la méthode addFiltersCriteria().
Pour filtrer les billets de notre blog en fonction d'un intervalle de nombre de commentaires, nous allons surcharger la méthode addFiltersCriteria() dans la classe postActions (back/modules/post/actions/actions.class.php) :

  protected function addFiltersCriteria($c)
  {
    // vérification si les champs ont été selctionnés pour filtrer
    $existMin = isset($this->filters['number_of_comments_min']) && $this->filters['number_of_comments_min'] !== '';
    $existMax = isset($this->filters['number_of_comments_max']) && $this->filters['number_of_comments_max'] !== '';
    $existMinAndMax = $existMin && $existMax;
   
    if ($existMinAndMax)
    {
      // ajout d'un criterion pour le minimum
      $criterion = $c->getNewCriterion(PostPeer::NUMBER_OF_COMMENTS, $this->filters['number_of_comments_min'], Criteria::GREATER_EQUAL);
      // ajout d'un criterion pour le maximum
      $criterion->addAND($c->getNewCriterion(PostPeer::NUMBER_OF_COMMENTS, $this->filters['number_of_comments_max'], Criteria::LESS_EQUAL));
      $c->add($criterion);
    }
    elseif ($existMin)
    {
      $c->add(PostPeer::NUMBER_OF_COMMENTS, $this->filters['number_of_comments_min'], Criteria::GREATER_EQUAL);
    }
    elseif ($existMax)
    {
      $c->add(PostPeer::NUMBER_OF_COMMENTS, $this->filters['number_of_comments_max'], Criteria::LESS_EQUAL);
    }
   
    parent::addFiltersCriteria($c);
  } 

Avec Symfony, pour écrire une requête à multiple conditions pour un champ, nous devons utiliser les criterions, sinon la 2e condition remplace la 1ère.

En cliquant sur le nombre de commentaires, nous accéderons à la liste des commentaires du billet.
Pour créer un lien dans une liste vers un autre module, nous devons utiliser un partial.
Dans back/modules/blog/config/generator.yml, nous remplaçons le champ number_of_comments par _list_number_of_comments (generator > param >list > display) et lui affectons un nom dans la section fields :

list_number_of_comments:   { name: Nb Comment. }

Nous créons le partial back/modules/blog/templates/_list_number_of_comments.php avec le code suivant :

<?php
if ($blog_post->getNumberOfComments()>0)
    echo link_to($blog_post->getNumberOfComments(), 'blogcomment/list', array('query_string' => 'filter=Filtrer&filters[blog_post_id]='.$blog_post->getId()));
else
    echo '0';
?>

Le 3e paramètre de link_to() est un tableau de paramètres. Nous indiquons à l'aide de query_string que l'url comportera un query string avec les options qui suivents.

Ici, notre url sera http://grunzig/back_dev.php/blogcomment/list?filter=Filtrer&filters[blog_post_id]=1

En suivant le lien, on s'aperçoit que la liste des commentaires n'est pas filtré. Il faut activer le filtre dans back/modules/blogcomment/config/generator.yml. En même temps, précisons les champs à afficher. Le fichier devrait ressembler à celui-ci :

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      BlogComment
    theme:            default
    fields:
        blog_post_title:   { name: Billet }
        author_name:    { name: Auteur }
        author_email:   { name: Mail }
        author_site:    { name: Site web }
        created_at:     { name: Créé le, params: date_format='dd/MM/yyyy H:mm' }
        body:           { name: Message }

    list:
      filters:          [blog_post_id]
      display:          [created_at, author_name, author_email, author_site, body]
      layout:           stacked
      params:   |
        le <strong>%%created_at%%</strong> par <strong>%%author_name%%</strong> <em>(%%author_email%% - %%author_site%%)</em>
        <p>%%=body%%</p>
      object_actions:
        _edit:        ~
        _delete:      ~

       
Nous obtenons alors :

Afin de masquer le formulaire de filtre, nous créons un partial (back/modules/blogcomment/templates/_filters.php) et nous le laissons vide. (Note: il existe peut être un autre moyen que je ne connais pas encore).  Ce partial sera utilisé à la place de _filters.php créé par l'admin-generator (disponible dans monprojet/cache/back/dev/modules/autoBlogcomment/templates/).

Pour l'édition d'un commentaire, nous allons dans un premier temps afficher le titre du billet à la place de son identifiant.
Dans back/modules/blogcomment/config/generator.yml, nous ajoutons :

    edit:
      title:            Détail du billet "%%blog_post_title%%"
      display:          [blog_post_title, author_name, author_email, author_site, created_at, body]
      fields:
        blog_post_title:{ type: plain }
        body:           { params: size=75x15 } 

Mais le champ blog_post_title n'existe pas ! et il n'existera pas réellement. Nous pouvons récupérer le titre  sans créer un nouveau champ.
Nous ajoutons la méthode suivante à la classe BlogComment (lib/model/BlogComment.php) :

  public function getBlogPostTitle()
  {
    return BlogPostPeer::retrieveByPK($this->blog_post_id);
  }
et celle-ci dans BlogPost (lib/model/BlogPost.php) :
  public function __toString()
  {
    return $this->getTitle();
  }

Ainsi, lors de l'affichage de blog_post_title, symfony utilise la méthode getBlogPostTitle() qui renvoie le titre du billet (et non pas un objet BlogPost) grâce à la méthode __toString() de BlogPost.

Lors de l'édition d'un billet, il serait appréciable de pouvoir accéder à la liste des commentaires associés. Nous allons nous en occuper. Je tiens tout de même à préciser que la présentation finale laisse à désirer et nécessiterait d'être refaite (CSS et HTML).

Ajoutons un partial pour BlogPost (back/modules/blogpost/templates/_edit_header.php) :

<ul class="navigation">
    <li>
        <?php echo link_to('Détail', $sf_request->getUri()); ?>
    </li>
    <li> | </li>
    <li>
<?php
if ($blog_post->getNumberOfComments()>0)
    echo link_to('Commentaires ('.$blog_post->getNumberOfComments().')', 'blogcomment/list', array('query_string' => 'filter=Filtrer&filters[blog_post_id]='.$blog_post->getId()));
else
    echo 'Commentaires (0)';
?>
    </li>
</ul>

Grâce à cela, lors de l'édition d'un billet, nous obtenons 2 liens avant le formulaire : un pour accéder à l'édition du billet et un pour accéder à la liste des commentaires.

Nous créons un 2e partial pour obtenir ce menu à partir de la liste des commentaires du billet (back/modules/blogcomment/templates/_list_header.php):

<?php $bpId = $sf_request->getParameter('filters[blog_post_id]') ?>
<ul class="navigation">
    <li>
        <?php echo link_to('Détail', 'blogpost/edit?id='.$bpId); ?>
    </li>
    <li> | </li>
    <li>
<?php
    echo link_to('Commentaires ('.BlogPostPeer::getNumberOfCommentsByPK($bpId).')', 'blogcomment/list', array('query_string' => 'filter=Filtrer&filters[blog_post_id]='.$bpId));
?>
    </li>
</ul>

et un 3 pour l'édition d'un commentaire (back/modules/blogcomment/templates/_edit_header.php) :

<ul class="navigation">
    <li>
        <?php echo link_to('Détail', 'blogpost/edit?id='.$blog_comment->getBlogPostId()); ?>
    </li>
    <li> | </li>
    <li>
<?php
    echo link_to('Commentaires ('.$blog_comment->getBlogPost()->getNumberOfComments().')', 'blogcomment/list', array('query_string' => 'filter=Filtrer&filters[blog_post_id]='.$blog_comment->getBlogPostId()));
?>
    </li>
</ul>

et nous ajoutons à la classe BlogPostPeer :

  public static function getNumberOfCommentsByPK($pk)
  {
    $bp = BlogPostPeer::retrieveByPK($pk);
    return $bp->getNumberOfComments();
  }


Note: je pense qu'il serait possible de faire la même chose avec un slot.

A suivre... l'ajout d'un système de tags (tags pour les billetq + TagCloud).

Un blog avec Symfony (7/)

Ajout des commentaires

Un blog sans possibilité d'effectuer des commentaires n'est pas vraiment un blog... Nous allons donc y remédier.

Dans un premier temps, il faut sauvegarder les données déjà enregistrer. Pour cela symfony dispose d'un outil qui va effectuer le travail à notre place.

$ symfony propel-dump-data back data.yml

Un fichier data.yml est créé dans projet/data/fixtures/ avec toutes les données déjà enregistrées.

Nous modifions a base de données à partir du fichier config/schema.yml :

  blog_comment:
    _attributes:      { phpName: BlogComment }
    id:              
    blog_post_id:
    author_name:      varchar(255)
    author_email:       varchar(255)
    author_site:        varchar(255)
    body:        longvarchar
    created_at:

et on ajoute à blog_post:

    number_of_comments : { type: integer, foreignTable: blog_post, foreignReference: id, onDelete: cascade }
 
On regénère la base, les classes, etc... et on insère les données sauvegardées au préalable

$ symfony propel-build-all-load back

On va tout d'abord afficher le nombre de commentaires dans la liste des billets.
On ajoute dans le fichier front/modules/blog/templates/listSuccess.php juste avant la dernière ligne :

<div><?php echo link_to($post->getNumberOfComments()?'Commentaires ('.$post->getNumberOfComments().')':'Commentaire (0)', '@blog_billet?annee='.$post->getCreatedAt('Y').'&mois='.$post->getCreatedAt('m').'&jour='.$post->getCreatedAt('d').'&title='.$post->getStrippedTitle()) ?></div>

Maintenant, on prépare l'affichage des billets lors de la vue d'un billet.
Dans front/modules/blog/templates/viewSuccess.php, on ajoute la ligne suivante juste après l'affichage du champ "body" :
<?php include_partial('blog/comment', array('post' => $post)) ?>

On obtient alors :
...
  <div class="post-content">
    <?php echo $post->getBody() ?>
  </div>

  <?php include_partial('blog/comment', array('post' => $post)) ?>

<?php else: ?>
  <p>
    Cette entrée n'a pu être trouvée dans le blog.
  </p>
<?php endif; ?>


La ligne que nous avons ajoutée fait appel à un "partial". Un partial est comme un template. Sa particularité est de pouvoir être appelée et réutilisée à partir d'une ligne, là où on le souhaite. Le nom de fichier du partial commence obligatoirement par le caractère _ .
A l'aide de cette ligne, nous précisons que le partial est situé dans le répertoire templates du module blog : 'blog/comment'.
De même, la variable $post utilisé dans le partial est la variable $post utilisé dans ce template : array('post' => $post).
Nous créons donc ce partial dans front/modules/blog/templates/. Le nom sera donc _comment.php et son contenu :

  <div class="post-comment">
    <h3>Les commentaires</h3>
    <!-- le nombre de commentaires -->
    <i><?php echo ($post->getNumberOfComments()==0) ? 'Soyez le premier !' : 'Il y en a '.$post->getNumberOfComments() ?></i>

    <?php $comments = $post->getBlogComments() ?>   
    <?php foreach($comments as $comment): ?>
    <div class="comment">

      <div class="author">
      <?php echo 'Le '.$comment->getCreatedAt('d/m/Y (H:m)').' par ' ?>
      <?php echo ($comment->getAuthorSite()!='') ? link_to($comment->getAuthorName(), $comment->getAuthorSite()) : $comment->getAuthorName(); ?>
      </div>
   
      <div class="body">
      <?php echo $comment->getBody() ?>
      </div>

    </div>
    <?php endforeach; ?>
   
    <!-- le formulaire d'ajout -->
    <?php use_helper('Validation') ?>
    <?php echo form_tag('blog/addComment') ?>
      <?php echo input_hidden_tag('post_id', $post->getId()) ?>
      <div>
        <?php echo form_error('name') ?>
        <label for="name">Nom (obligatoire)</label>
        <?php echo input_tag('name', '', 'id= class=text') ?>
      </div>
      <div>
        <?php echo form_error('mail') ?>
        <label for="mail">Mail (obligatoire) (ne sera pas publié)</label>
        <?php echo input_tag('mail', '', 'id= class=text') ?>
      </div>
      <div>
        <?php echo form_error('website') ?>
        <label for="url">Site internet</label>
        <?php echo input_tag('website', '', 'id= class=text') ?>
      </div>
      <div>
        <?php echo form_error('body') ?>
        <label for="body">Message</label>
        <?php echo textarea_tag('body', '', 'id=') ?>
      </div>
      <div>
        <?php echo submit_tag('Valider') ?>
      </div>
    </form>

  </div>

A la ligne 24, form_tag() nous permet de créer la balise d'ouverture d'un formulaire. echo form_tag('blog/addComment') peut se traduire par afficher la balise d'ouverture du formulaire avec en paramètres le module blog et l'action addComment.
A la ligne 27, echo form_error('name') permettra l'affichage d'un message d'erreur correspondant au champ "name".

Pour traiter les informations envoyées par vos lecteurs, nous allons créer 2 méthodes dans la classe blogActions (fichier front/modules/blog/actions/actions.class.php) :

  public function executeAddComment()
  {
    // on recherche le billet auquel le commentaire se rapporte
    // on utilise post_id (champ caché dans le formulaire)
    $post = PostPeer::retrieveByPK($this->getRequestParameter('post_id'));
   
    // on prépare la requête pour l'ajout dans la base
    $comment = new BlogComment();
    $comment->setBlogPostId($post->getId());
    $comment->setAuthorName(strip_tags($this->getRequestParameter('name')));
    $comment->setAuthorEmail(strip_tags($this->getRequestParameter('mail')));
    $url = strip_tags($this->getRequestParameter('website', ''));
    if($url!='')
    {
      // si besoin, on ajoute http:// à l'url, sinon elle s'affichera comme une url interne à l'application
      if(strpos($url, 'http://') !== 0) $url = 'http://'.$url;
      $comment->setAuthorSite($url);
    }
    $comment->setBody(strip_tags($this->getRequestParameter('body')));
    // on enregistre les données
    $comment->save();
   
    // Mise à jour du nombre de commentaires dans la table des billets
    PostPeer::doIncreaseNumberOfComments($post);
   
    // on redirige vers le billet courant
    $this->redirect('blog/view?annee='.$post->getCreatedAt('Y').'&mois='.$post->getCreatedAt('m').'&jour='.$post->getCreatedAt('d').'&title='.$post->getStrippedTitle());
  }
 
  public function handleErrorAddComment()
  {
    // en cas d'erreur on réaffiche la page du billet
    $this->forward('blog', 'view');
  }

Pour mettre à jour le nombre de commentaire pour un billet, nous faisons appelle à la méthode static PostPeer::doIncreaseNumberOfComments()
Mais elle n'existe pas ! pas encore ;-)

On ouvre donc le fichier lib/model/PostPeer.php et on ajoute à la classe :

  public static function doIncreaseNumberOfComments(&$post)
  {
    PostPeer::doUpdateNumberOfComments($post, $post->getNumberOfComments()+1);
  }

  public static function doDecreaseNumberOfComments(&$post)
  {
    PostPeer::doUpdateNumberOfComments($post, $post->getNumberOfComments()-1);
  }
 
  private static function doUpdateNumberOfComments(&$post, $nbOC)
  {
    $c = new Criteria();
    $c->add(PostPeer::ID, $post->getId());
    $c->add(PostPeer::NUMBER_OF_COMMENTS, $nbOC);
    PostPeer::doUpdate($c);
  }

Et pour valider le formulaire on crée un fichier front/modules/blog/validate/addComment.yml avec :

fillin:
  enabled: true
  param:
    name: add_comment
 
fields:
  name:
    required:
      msg: Vous devez indiquer votre nom ou un pseudo
    sfStringValidator:
      min: 4
      min_error: Votre nom doit comporter au moins 4 caractères
      max: 100
      max_error: Votre nom doit comporter au plus 100 caractères
  mail:
    required:
      msg: Vous devez indiquer votre email
    sfEmailValidator:
      email_error: Cette adresse email n'est pas valide
  body:
    required:
      msg: Vous devez saisir un message
    sfStringValidator:
      min: 4
      min_error: Votre message doit comporter au moins 4 caractères

Après quelques tests, nous obtenons quelques choses comme ça :

Niveau présentation, ce n'est vraiment pas terrible mais pour l'instant c'est php et symfony, pas CSS ;-)

 

Un blog avec Symfony (6/)

Ajout de critères de sélection en frontoffice

Ajoutons la possibilité de n'afficher que les billets pour une date donnée (année, ou année et mois, ou année, mois et jour) ou par catégorie.

Dans lib/model/Blogcategory.php, on ajoute :
  public function setName($v)
  {
    parent::setName($v);
 
    $this->setStrippedName(mesOutils::stripText(mesOutils::unAccent($v)));
  }


Dans front/modules/blog/templates/ pour listSuccess.php et viewSuccess.php, après l'affichage de l'heure :
   <?php $vBlogCategory = $post->getBlogCategory(); ?>
    <?php if ( $vBlogCategory != ''): ?>
         dans <?php echo link_to($vBlogCategory, 'blog/'.$vBlogCategory->getStrippedName()) ?>
    <?php endif; ?>

 
Et on modifie front/modules/blog/actions/actions.class.php afin qu'il ressemble à ceci :
class blogActions extends sfActions
{
  public function executeIndex()
  {
    $this->forward('default', 'module');
  }
 
  public function executeList()
  {
    $c = new Criteria();
    $this->makeQuery($c);
    $this->posts = PostPeer::doSelect($c);   
  }
 
  public function executeView()
  {
    $c = new Criteria();
    $this->makeQuery($c);
    $this->posts = PostPeer::doSelect($c);
   
    // on modifie dynamiquement le titre de la page à l'aide de l'objet Response accessible à partir de sfActions
    // $response = $this->getResponse();
    // $response->setTitle('Grunzig.net - '.$this->posts[0]->getTitle());
  }
 
  private function makeQuery(&$c)
  {
    // url : blog/[annee/[mois/[jour/[titre.html]]]]
    if ( $this->getRequestParameter('annee')!= '' )
    {
      $c->add(PostPeer::CREATED_AT, $this->getRequestParameter('annee').'%', Criteria::LIKE);

      if ( $this->getRequestParameter('mois') != '' )
      {
        $c->add(PostPeer::CREATED_AT, $this->getRequestParameter('annee').'-'.$this->getRequestParameter('mois').'%', Criteria::LIKE);
       
        if ( $this->getRequestParameter('jour') != '' )
        {
          $c->add(PostPeer::CREATED_AT, $this->getRequestParameter('annee').'-'.$this->getRequestParameter('mois').'-'.$this->getRequestParameter('jour').'%', Criteria::LIKE);
        }
      }
    }

    if ( $this->getRequestParameter('categorie') != '' )
    {
      $c->add(BlogCategoryPeer::STRIPPED_NAME, $this->getRequestParameter('categorie'));
      $c->addJoin('blog_post.BLOG_CATEGORY_ID', 'blog_category.ID', Criteria::LEFT_JOIN);
    }

  if ( $this->getRequestParameter('title') != '' )
    {
      $c->add(PostPeer::STRIPPED_TITLE, $this->getRequestParameter('title'));
    }
    $c->addDescendingOrderByColumn(PostPeer::CREATED_AT);
  }
}


Dans apps/front/config/routing.yml :
blog_annee:
  url:   /blog/:annee
  param: { module: blog, action: list }
  requirements: { annee: ^\d{4}$ }

blog_annee_mois:
  url:   /blog/:annee/:mois
  param: { module: blog, action: list }
  requirements: { annee: ^\d{4}$, mois: ^\d\d$ }

blog_annee_mois_jour:
  url:   /blog/:annee/:mois/:jour
  param: { module: blog, action: list }
  requirements: { annee: ^\d{4}$, mois: ^\d\d$, jour: ^\d\d$ }

blog_categorie:
  url:   /blog/:categorie
  param: { module: blog, action: list }

Un blog avec Symfony (5/)

Validation des champs des formulaires des billets et des catégories

Dans une application, c'est capital. Ceci aurait du être fait depuis la création des modules mais je suis parti du principe que j'étais le seul à écrire des billets et donc que je ne devais pas faire d'erreur. Malgré cela, il est préférable de les contrôler.

Il faut écrire un fichier YAML pour définir les contraintes des données à enregistrer. Dans back/modules/post/validate/, on ajoute un fichier edit.yml (edit pour l'ajout et la modification des données).

fields:
  post{title}:
    required:
        msg: Le titre est obligatoire
    sfStringValidator:
      min:       10
      min_error: C'est trop court ! (10 caractères minimum)
      max:       100
      max_error: C'est trop long !. (100 caractères maximum)     
     
  post{excerpt}:
    required:
        msg: L'extrait est obligatoire
    sfStringValidator:
      min:       10
      min_error: C'est trop court ! (10 caractères minimum)
      max:       100
      max_error: C'est trop long !. (100 caractères maximum)     

  post{body}:       
    required:
        msg: Le contenu est obligatoire
    sfStringValidator:
      min: 100
      min_error: C'est trop court ! (100 caractères minimum)

On précise les champs obligatoires, le nombre minimum et maximum de caractères en fonction des champs. Ces instructions sont facultatives et d'autres existent.
Attention : Pour indiquer le nom du champ, il faut indiquer le nom de la table de la base de données suivi du nom du champ entre accolades (ex: post{title} ).

On répète la même opération pour les autres modules...

Migration de SQLite vers MySQL

Pour des raisons pratiques, je passe pour le développement de SQLite à MySQL.
Il faut configurer symfony pour qu'il prenne en compte MySQL.
Dans le fichier projet/config/databases.yml :
dev:
  propel:
    class:     sfPropelDatabase
    param:
      phptype:            mysql
      hostspec:           localhost
      database:           grunzig
      username:           user
      password:           pass
      port:               80
      encoding:           utf8
      persistent:         true
Dans projet/config/propel.ini :
propel.database            = mysql
propel.database.createUrl  = mysql://localhost/
propel.database.url        = mysql://user:pass@localhost/grunzig


Ensuite on génère le modèle, la création des tables et on crée les tables :
$ symfony propel-build-model
$symfony propel-build-sql
$ symfony propel-insert-sql

Affichage des billets côté frontoffice

Il n'y a rien à faire de particulier. En allant à l'adresse http://grunzig/front_dev.php/blog/list, la liste des billets s'affichent car nous avions déjà créer le module.

Pour afficher un seul billet, il faut définir une route. Dans apps/front/config/routing.yml :
blog_billet:                                                        # nom unique pour la route
  url:   /blog/:annee/:mois/:jour/:title.html                       # l'url avec la place des paramètres
  param: { module: blog, action: view }                             # le module appelé et son action
  requirements: { annee: ^\d{4}$, mois: ^\d\d$, jour: ^\d\d$ }      # les contraintes pour annee, mois et jour

Dans apps/front/modules/blog/templates/listSuccess.php, on transforme le titre du billet en lien vers le détail. Ce lien doit être conforme à la route spécifiée dans routing.yml.
<?php echo link_to($post->getTitle(), '@blog_billet?annee='.$post->getCreatedAt('Y').'&mois='.$post->getCreatedAt('m').'&jour='.$post->getCreatedAt('d').'&title='.$post->getTitle()) ?>

L'url vers un billet n'est pas très lisible. Pour cela, nous allons créer une version alternative du titre. Nous allons stocker ce titre alternative dans un nouveau champ de la base de données.
On modifie config/schema.yml comme ceci :
propel:
  blog_post:
    _attributes: { phpName: Post }
    id:
    title:          varchar(255)
    stripped-title: varchar(255)
...


On reconstruit le modèle et les commandes SQL :
$ symfony propel-build-model
$ symfony propel-build-sql


Et on met à jour la base de données.

Création d'un titre "dépouillé"

Il serait souhaitable que lors de la création ou de la modification d'un titre, le titre dépouillé soit automatiquement enregistrer. Comme le "dépouillage" pourra également être appliqué aux catégories, nous allons créer une classe personnalisée afin de ne pas dupliquer le code chaque fois que nous en aurons besoin.

Créons un fichier mesOutils.class.php dans grunzig/lib :
<?php

class myTools
{
  public static function stripText($text)
  {
    $text = strtolower($text);

    // strip all non word chars
    $text = preg_replace('/\W/', ' ', $text);

    // replace all white space sections with a dash
    $text = preg_replace('/\ +/', '-', $text);

    // trim dashes
    $text = preg_replace('/\-$/', '', $text);
    $text = preg_replace('/^\-/', '', $text);

    return $text;
  }

  public static function unAccent($text)
  {
    // Table des entités dans un tableau
    $trans = get_html_translation_table(HTML_ENTITIES);
    // 2 tableaux : un pour les caractères accentués et un pour les autres
    foreach ($trans as $litteral => $entity)
    {
        // On ignore les autres caractères (fractions, quotes etc)
        if (ord($litteral) >= 192)
        {
            // Récupère le 'E' de '&Eaccute' etc.
            $replace[] = substr($entity, 1, 1);
            // Récupère la forme accentuée
            $search[] = utf8_encode($litteral);
        }
    }
    return str_replace($search, $replace, $text);
  }
}

?>

Placer dans le dossier lib, la classe mesOutils sera automatiquement chargée par symfony lorsque ce sera nécessaire.

Maintenant, ajoutez dans le fichier lib/model/Post.php les lignes suivantes :
  public function setTitle($v)
  {
    parent::setTitle($v);
 
    $this->setStrippedTitle(mesOutils::stripText(mesOutils::unAccent($v)));
  }

On supprime le cache et voilà !

Il faut également modifier front/modules/blog/templates/viewSuccess.php et front/modules/blog/templates/listSuccess.php pour qu'il prenne en compte les modifications :

<?php echo link_to($post->getTitle(), '@blog_billet?annee='.$post->getCreatedAt('Y').'&mois='.$post->getCreatedAt('m').'&jour='.$post->getCreatedAt('d').'&title='.$post->getStrippedTitle()) ?>

Dans front/modules/blog/actions/actions.class.php, il est nécessaire de remplacer :
       $c->add(PostPeer::TITLE, $this->getRequestparameter('title'));
par
       $c->add(PostPeer::STRIPPED_TITLE, $this->getRequestparameter('title'));

Un blog avec Symfony (4/)

Création d'un menu dans le backoffice

Pour passer de la gestion des catégories à celles des billets, ce n'est pas très pratique.
On modifie le fichier apps/back/templates/layout.php :

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>

<?php include_http_metas() ?>
<?php include_metas() ?>

<?php include_title() ?>

<link rel="shortcut icon" href="/favicon.ico" />

<style type="text/css">
    #navigation ul {margin: 0px; padding: 0px}
    #navigation li {display: inline; list-style-type: none}
    #navigation a {padding: 3px 10px}
    #navigation a:link, #navigation a:visited {color: white; background-color: grey; text-decoration: none;}
    #navigation a:hover{color: black; background-color: #FFFFCC; text-decoration: none;}
</style>

</head>
<body>

<div id="navigation">
  <ul>
    <li><?php echo link_to('Billets', 'post/list') ?></li>
    <li><?php echo link_to('Catégories', 'blogcategory/list') ?></li>
  </ul>
</div>

<?php echo $sf_data->getRaw('sf_content') ?>

</body>
</html>


Filtrage des billets par catégories

Il serait intéressant de pouvoir filtrer par catégorie et d'afficher leur nom dans la liste des billets.
Dans apps/back/modules/post/config/generator.yml, on ajoute sous
generator:
  param:

    fields:
      blog_category_id:    { name: Catégorie }

    list:
      fields:
        category_name: { name: Catégorie}
      filters: [title, blog_category_id, created_at]

    edit:
      fields:
        blog_category_id:


et dans lib/model/Post.php, on ajoute dans la classe Post :
public function getCategoryName()
  {
       return BlogCategoryPeer::retrieveByPK($this->blog_category_id);
  }


Sécurisation de l'accès au backoffice

Rien ne nous permet de s'authentifier.  Nous ajoutons un module pour cela :
$ symfony init-module back security
On ajoute le formulaire de saisie du login et du mot de passe dans apps/back/modules/security/templates/indexSuccess.php :

<h2>Authentification</h2>
 
<?php if ($sf_request->hasErrors()): ?>
  L'identifiant et / ou le mot de passe ne correspondent pas.
<?php endif; ?>
 
<?php echo form_tag('security/login') ?>
  <label for="login">Login :</label>
  <?php echo input_tag('login', $sf_params->get('login')) ?>
 
  <label for="password">Mot de passe :</label>
  <?php echo input_password_tag('password') ?>
 
  <?php echo submit_tag('submit', 'class=default') ?>
</form>

Dans security/actions/actions.class.php, on supprime le contenu de executeIndex() (il n'y a pas besoin de la redirection) et on ajoute le code ci-dessous pour vérifier le login et le mot de passe saisie:

  public function executeLogin()
  {
    if ($this->getRequestParameter('login') == 'admin' && $this->getRequestParameter('password') == 'password')
    {
      $this->getUser()->setAuthenticated(true);
  
      return $this->redirect('post/index');
    }
    else
    {
      $this->getRequest()->setError('login', 'incorrect entry');
  
      return $this->forward('security', 'index');
    }
  }


Il faut préciser que le module par défaut est security dans back/config/settings.yml :
all:
  .actions:
    login_module:           security   # To be called when a non-authenticated user
    login_action:           index     # Tries to access a secure page


Par défaut, toute l'application back sera sécurisée.
On passe la valeur de is_secure à on dans back/config/security.yml (j'ignore encore pour quelle raison mais ce fichier security.yml n'est pas à utiliser par défaut. Au vu de son emplacement, il me semblait logique qu'il définisse les paramètres par défaut de tous les modules de l'application back, ce qui est partiellement vrai. Tous les modules nécessitent une authentification pour y accéder mais la récupération des messages des erreurs dans le template indexSuccess.php des modules ne fonctionne pas, ainsi que la validation des login et mot de passe).
Il existe 2 méthodes pour faire cela. La première consiste à sécuriser l'accès à l'application back par défaut et la deuxième permet de sécuriser module par module.


Première méthode

Il faut créer un fichier security.yml dans back/config et y ajouter :

all:
  is_secure: on

Ainsi, aucun module n'est accessible sans être authentifié, le module security inclus. Or, c'est ce module qui permet de gérer l'authentification : il doit donc être accessible à tout le monde. On ajoute un fichier security.yml dans back/modules/security/config avec le contenu suivant :

all:
  is_secure: off


Deuxième méthode

Il faut donc créer un fichier security.yml dans le répertoire config de chaque module. Ce fichier doit contenir :

all:
  is_secure: on


Le tour est joué !

On ajoute un moyen pour se déconnecter proprement.
On ajoute dans back/modules/security/actions/actions.class.php :

  public function executeLogout()
  {
    if ($this->getUser()->isAuthenticated())
    {
      $this->getUser()->setAuthenticated(false);
     
      return $this->forward('security', 'index');
    }
  }

et un lien dans le menu de navigation (back/templates/layout.php) :

 

Un blog avec Symfony (3/)

Affichage des billets en frontoffice

Nous allons nous occuper de l'affichage en front office.
Modifier le fichier apps/front/config/routing.yml comme cela :
homepage:
  url:   /
  param: { module: blog, action: list }

Ainsi, le module qui sera utilisé à la racine du site sera le module "blog" avec l'action "list"

On crée l'action list dans apps/front/modules/blog/actions/actions.class.php en ajoutant dans la classe blogActions :
  /**
  * Executes list action
  *
  */
  public function executeList()
  {
    $c = new Criteria();
    $this->posts = PostPeer::doSelect($c);
  }


On crée listSuccess.php dans apps/front/modules/blog/templates/ et on insère :

<?php foreach ( $posts as $post ): ?>
  <h2 id="post-<?php echo $post->getId() ?>">
    <?php echo link_to($post->getTitle(), $post->getCreatedAt('d/m/Y | H:i').'&post_title='.$post->getTitle()) ?>
  </h2>
  <p class="post-info">
    publié le <?php echo $post->getCreatedAt('d/m/Y à H:i') ?>
  </p>
  <div class="post-content">
    <?php echo $post->getBody() ?>
  </div>

<?php endforeach; ?>

Et on obtient :

 

Définir le module par défaut

Pour afficher à la racine du site les billets, il faut le paramétrer dans apps/front/modules/config/routing.yml :
homepage:
  url:   /
  param: { module: blog, action: list }

Création des catégories

Organiser les billets dans des catégories seraient une bonne idée. Et en cherchant bien, nous découvrons qu'il n'est pas possible d'ajouter facilement un champ dans une base de données avec symfony !
Comme nous travaillons sur une base de données de test, le plus simple est de modifier la table blog_post dans config/schema.yml en ajoutant  blog_category_id et de créer la table pour les commentaires :
  blog_category:
    _attributes:      { phpName: BlogCategory }
    id:              
    name:    { type: varchar(255) }

Dans la table blog_post, le champ blog_category_id n'a pas besoin de paramètre ; symfony sait reconnaitre une clé étrangère : _id  signifie que ce champ est une clé étrangère et blog_category indique que cette clé correspond au champ id de la table bog_category. Ainsi, une liste déroulante pourra être générée automatiquement dans l'interface d'administration pour sélectionner la catégorie par son identifiant.
Pour le faire via le nom de la catégorie, ajoutons la méthode __toString() à la classe lib/model/BlogCategory.php :
class BlogCategory extends BaseBlogCategory
{
  public function __toString()
  {
    return $this->getName();
  }
}


Ensuite, on re génère le modèle, on crée les commandes SQL et on re crée la base de données
$ symfony propel-build-model
$ symfony propel-build-sql
$ symfony propel-insert-sql

Nous générons le module d'administration  Category côté backoffice :
$ symfony propel-init-admin back blogcategory BlogCategory

On arrange un peu le tout...
Dans le fichier apps/back/modules/blogcategory/config/generator.yml :
generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      BlogCategory
    theme:            default
   
    fields:
        name:   {name: Nom}
       
    list:
        title:          Liste des catégories des billets
        object_actions:
            _edit:      ~
            _delete:    ~
        layout:         tabular
           
           
    edit:
        title:      Détail de la catégorie des billets
        fields:
            name:   {type: input_tag, params: size=50}

Un blog avec Symfony (2/)

Création de l'application "front"

Le projet disposera de 2 parties distinctes : une visible par tous les internautes ("front" pour "frontoffice") et la partie administration ("backoffice")qui sera accessible uniquement pour les personnes autorisées. Le frontoffice et le backoffice sont donc les applications du projet.

L'application "front" a déjà été créer afin de configurer Apache (voir Installer Apache, PHP, MySQL et Symfony sous Ubuntu).
Nous allons donc créer le module "blog" pour l'application "front" :

$ symfony init-module front blog


Votre module de blog est maintenant créé. Il est accessible à l'adresse http://grunzig/index.php/blog
Cette page nous indique que nous avons créer notre module "blog" :

Il faut maintenant créer l'interface pour créer de nouveaux billets.

Création de l'interface de gestion du blog

Dans un premier temps, il est nécessaire de créer l'application "back" :
$symfony init-app back


Ensuite, nous allons créer une interface de gestion :
$ symfony propel-init-admin back post Post

Cette commande permet de générer un module "post" dans l'application "back". Ce module est basée sur la défénition de classe "Post" .
Comme nous travaillons en environnement de développement, le module est accessible vie l'url http://grunzig/back_dev.php/post



Nous pouvons maintenant enregistrer notre premier billet.


"Francisation" de l'interface d'administration

Nous allons franciser un peu le tout et modifier l'affichage.
Dans la liste des billets, affichage des champs Titre (title), Extrait (excerpt) et Date de création (created_at). On précise également le nombre de billets affiché : 5 (max_per_page. On ajoute un filtre sur le titre et la date de création.
Dans la page de création / modification d'un billet, affichage des champs Titre (title), Extrait (excerpt), Contenu (body) et Date de création (created_at)
Pour cela, on modifie le fichier back/modules/post/config/generator.yml :

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Post
    theme:            default
    fields:
      title:          { name: Titre }
      excerpt:        { name: Extrait }
      body:           { name: Contenu }
      created_at:     { name: Date de création }
    list:
      title:          Liste des billets
      layout:         tabular
      display:        [=title, excerpt, created_at]
      object_actions:
        _edit:        ~
        _delete:      ~
      max_per_page:   5
      filters:        [title, created_at]
    edit:
      title:          Détail du billet
      fields:
        title:        { type: input_tag, params: size=53 }
        excerpt:      { type: textarea_tag, params: size=50x2 }
        body:         { type: textarea_tag, params: size=50x10 }
        created_at:   { type: input_date_tag, params: rich=on }

Et on obtient :

Ce n'est pas encore tout à fait ça. Les boutons ne sont pas traduits.
Tout d'abord, il fauit modifier le fichier apps/back/config/il8n.yml en changent la paramètre default_culture en fr_FR
Ensuite, il faut activer la traduction. Dans le fichier apps/back/config/settings.yml, il faut ajouter :
all:
    .settings:
         il8n:     on

Ensuite, nous allons créer un fichier messages.fr.xml dans le répertoire apps/back/il8n/ et écrire les lignes suivantes :



 
   
     
        reset
        Effacer
     
     
        filter
        Filtrer
     
     
        filters
        Filtres
     
     
        [0] no result|[1] 1 result|(1,+Inf] %1% results
        [0] Aucun résultat|[1] 1 résultat|(1,+Inf] %1% résultats
     
     
        create
        Nouveau
     
     
        list
        Liste
     
     
        save
        Valider
     
     
        save and add
        Valider et ajouter
     
     
        Your modifications have been saved
        Vos modifications ont été prises en compte
     
     
        delete
        Supprimer
     
     
        Are you sure?
        Etes vous sûr ?
     
     
        edit
        Modifier
     
   
 


On enregistre le fichier et on réactualise la page web.
Apparemment, il n'y aucun changement.
Supprimer le cookie lié au domaine ou redémarrer votre navigateur et la magie se produit... Explication : le paramètre de la culture est stockée durant la session dans un cookie.

Installation de TinyMCE

Nous allons installer TinyMCE (un "rich text editor") sur notre blog.
Télécharger le "Main Package" sur http://tinymce.moxiecode.com/download.php
Décompresser le fichier dans web/js
Les fichiers de langues sont disponibles à http://services.moxiecode.com/i18n/
Dans le fichier apps/back/config/settings.yml, sous "  .settings:", nous indiquons l'emplacement de TinyMCE :
    rich_text_js_dir:       js/tiny_mce"

Dans le fichier apps/back/modules/post/config/generator.yml, il est possible de préciser la langue de TinyMCE :
        body:         { type: textarea_tag, params: size=50x10 rich=true }

Un blog avec Symfony (1/)

Un blog; par principe, contient des billets. Il est généralement possible de laisser la possibilité de commenter des billets. Ces billets sont généralement liés à des catégories.
Toutes ces informations sont stockées dans une base de données. Pour le développement, nous allons utiliser une base de données de type SQLite.

Configuration de la base de données

Editer le fichier config/databases.yml et modifier le le comme indiqué ci-dessous :


dev spécifie l'environnement développement
phptype indique le type de base de données (SQLite)
database précise le nom de la base de données. Comme nous utilisons SQLite, nous indiquons également l'emplacement de ce fichier. %SF_DATA_DIR% est une constante ayant pour valeur "nomduprojet/data", donc, grunzig/data pour moi.
Il sera peut être nécessaire de modifier le fichier config/propel.ini

 

Création de la table des billets

Créons un fichiers schema.yml dans le dossier de configuration du  projet (mon projet s'appelle "grunzig") : grunzig/config/schema.yml
Enregistrer le fichier puis ouvrir une console et aller dans le répertoire du projet : $ php symfony propel-build-model

Des classes viennent d'être créer dans lib/model :

Ce sont les classes de qui nous permettent d'avoir accès à une base de données relationnelle à partir d'un code orienté objet sans écrire une seule requête SQL. Symfony utilise la bibliothèque Propel à cette fin. Nous appellerons ces objets le modèle (plus trouver dans le modèle chapitre).

$ php symfony propel-build-sql

Un fichier lib.model.schema.sql est créé dans le répertoire data/sql/.



Ce fichier est utilisé pour générer les tables de l'application




On insère ensuite les les données dans la base. Comme nous utilisons SQLite, le fichier de la base de données est créer en même temps.

Nous pouvons remarquer qu'une erreur est affichée. Rien de grave, Propel a essayé de supprimer la table "blog_post". Comme nous ne l'avions pas encore créée, elle ne peut pas être supprimée et génère une erreur.

Note : il faut modifier les droits du fichier de base de données pour ensute pouvoir insérer des données via le navigateur web :
$ chmod data/grunzig.db 777

Ajouter la coloration syntaxique pour les fichiers YAML dans Eclispe

Afin d'ajouter la coloration syntaxique pour les fichiers YAML dans Eclipse, il faut aller dans le menu :

Help > SOftware Update > Find and Install

Puis selectionner :

Search for new features to install

Cliquer sur Next puis New Remote Site

Dans le champ Name, on met YamlEditor et pour l'url http://grupy.net/yamlEditor/

Valider 2 fois de suite.

Sélectionner YamlEditor, puis cliquer sur Next.

Accepter les termes de la licence.

Cliquer sur Next, Finish et Install.

Relancer Eclispe, et voilà !

Obtenir la complétion de code pour Symfony (Eclipse+PDT)

Pour obtenir la complétion de code pour Symfony dans le logiciel Eclipse, il existe 2 solutions.

Ajouter Symfony comme librairie

Dans Eclispe, ouvrir le navigateur PHP
Faire un clic droit sur le projet
Cliquer sur "Configure Include Path"
Sélectionner l'onglet "Libraries"


Cliquer sur "Add External Folder"
Aller vers "\route\to\PEAR\symfony"
Valider avec "Finish"

Créer un dossier virtuel vers les librairies symfony

Afin de pouvoir naviguer et visualiser le code de Symfony à l'aide d'Eclipse, il est nécessaire d'ajouter le dossier "/route/to/PEAR/symfony" comme répertoire virtuel (et non pas comme librairie).

Clic droit sur le répertoire "lib" du projet
Cliquer ensuite sur "Configure Include Path"
Sélectionner l'onglet "Libraries"
Cliquer sur "Add variable"
"Configure variables"
Cliquer sur "Advanced"
Puis sur "New"
Entrer "Name : symfony ; Path : /var/share/php5/symfony"
Valider avec "OK"
Selectionner "symfony" et cliquer sur "OK"

Installer Apache, PHP, MySQL et Symfony sous Ubuntu

Installer Apache

Dans une console, saisir sudo apt-get install apache2

Valider la demande.

En saisissant http://localhost/ dans un navigateur web, on constate que Apache fonctionne.

Installer PHP 5

Dans une console, saisir sudo apt-get install php5. Les dépendances et le module php pour Apache seront installés en même temps.

Valider l'installation.

Procéder de la même manière pour installer php en ligne de commande (php-cli) :

Ensuite, vérifier que apache et mysql sont actifs en saisissant dans un navigateur web : http://localhost/

Installer MySQL

Dans une console, saisir sudo apt-get install mysql-server

Puis valider pour continuer, après l'invitation.

Entrez un nouveau mot de passe adminsitrateur pour MySQL lors de l'invite et validez

L'installation et la configuration se poursuivent.


Voilà, MySQL Server est installé !

Installer Symfony

Ajouter : deb http://www.symfony-project.org/get debian/
dans la liste des sources (fichier /etc/apt/sources.list)
Mettre à jour la liste des paquets :
sudo apt-get update


Lancer l'installation :
sudo apt-get install php5-symfony



Les librairies de symfony sont installées ainsi :
$php_dir/symfony/    /usr/share/php/symfony/         libraries de bases
$data_dir/symfony/    /usr/share/php/data/symfony/    squelette des applications symfony, modules par défaut et configuration
$doc_dir/symfony/    /usr/share/php/docs/symfony/    documentation

Dans une console :

Créer un dossier pour le projet dans /var/www/ et aller dans ce dossier:
mkdir /var/www/grunzig
cd /var/www/grunzig


Créer le projet
symfony init-project grunzig


Créer l'application (ici "front")
symfony init-app front


Configurer Apache
sudo gedit /etc/apache2/sites-available/grunzig

Saisir le texte suivant dans Gedit :

AllowOverride All
Allow from All


ServerName monprojet
DocumentRoot "/var/www/
grunzig/web"
DirectoryIndex index.php
Alias /sf /usr/share/php/data/symfony/web/sf

grunzig/web">
AllowOverride All
Allow from All

Modifier les DNS de Ubuntu pour qu'il reconnaisse grunzig (http://grunzig/ pointera sur le virtual directory créé)
sudo gedit /etc/hosts

Ajouter :
127.0.0.1 grunzig

Autoriser l'url rewriting avec la commande suivante :
a2enmod rewrite


Editer /etc/php5/apache2/php.ini, et modifier la ligne suivante :
magic_quotes_gpc = On
en
magic_quotes_gpc = Off

Redémarrer Apache:
sudo apache2 -k restart

Dans le navigateur internet, pour vérifier que tout fonctionne cortrectement, saisissez http://grunzig/