J’ai récemment buté sur un problème avec Symfony: dans WordPress, nous pouvons assigner plusieurs catégories pour un même article, le choix s’effectuant avec des checkbox, et je voulais refaire la même chose pour mon projet de blog (voir l’image de droite). Le truc, c’est que j’avais soit le choix de faire un select qui accepte plusieurs sélections, mais pas très ergonomique et de toute manière aussi peu pratique à mettre en place, soit faire une même liste de checkbox, ce pour quoi j’ai opté…
La doc’ concernant cette problématique est pour ainsi dire quasi inexistante !
Donc voila, j’ai ma liste de catégories de ma class Category, j’ai mes articles de ma class Article et enfin la class qui joint les 2 nommée Article_category.
Il va falloir générer dynamiquement les widgets et les validateurs pour la class de notre formulaire ArticleForm, et faire la même chose du côté du template. Ensuite il va falloir faire le traitement des données et la sauvegarder, mais vous verrez qu’à défaut de documentation, j’ai directement bidouillé la sauvegarde de mes objets Article_category dans l’action…
Pour résumer, les implémentations dans ma class de formulaire et dans mon template sont propres, mais la sauvegarde c’est pas encore ça !
Tout d’abord, on va générer les widgets et validateurs via les catégories existantes:
// lib/form/doctrine/ArticleForm.class.php
//...
$this->widgetSchema['category'] = new sfWidgetFormSchema();
$this->validatorSchema['category'] = new sfValidatorSchema();
foreach (Category::getAllCategories() as $category)
{
$this->widgetSchema["category"][$category->getId()] = new sfWidgetFormInputCheckbox();
$this->validatorSchema["category"][$category->getId()] = new sfValidatorPass();
$this->widgetSchema["category"]->setLabel($category->getId(), $category->getName());
}
//...
Vous pouvez constater que je mets un validateur sfValidatorPass, qui retourne la valeur entrée par l’utilisateur tel quel ! On ne test de toute manière que null ou not null pour une checkbox, donc si la value est remplit avec n’importe quoi, après on s’en fiche.
Sinon, les clefs de mes validateurs et widgets sont les id des catégories, pour pouvoir les récupérer comme on va voir un peu plus loin.
Maintenant, on va générer côté template, avec le même principe:
// apps/myApp/modules/article/templates/newSuccess.php
...
<tr>
<td>Categories: </td>
<td>
<table>
<?php
// Méthode statique qui va chercher toutes mes catégories
foreach(Category::getAllCategories() as $category)
{
echo "<tr><td>".$form["category"][$category->getId()]->renderError().$form["category"][$category->getId()]->renderLabel()."</td>";
echo "<td>";
echo $form["category"][$category->getId()]->render();
echo "</td></tr>";
}
?>
</table>
</td>
</tr>
...
Notez que par habitude, je sépare le template de mon formulaire pour les actions new et edit, une mauvaise habitude que je vais changer au plus vite :p A vous d’adaptez le code pour _form.php.
Notre template est pas mal, mais si vous éditez un article, se serait bien plus pratique d’afficher les catégories liées cochées (avec l’attribut checked). On va devoir créer une méthode statique qui teste pour chaque catégorie si un objet Article_category existe dans la base de données, et si c’est le cas, on ajoute l’attribut checked:
La méthode:
// lib/model/doctrine/Article.class.php
public function isACurrentCategoryOfArticle($article)
{
$q = Doctrine_Query::create()
->from('Article_category ac')
->where('ac.category_id = ?', $this->getId())
->andWhere('ac.article_id = ?', $article->getId());
if ($articleCategory = $q->fetchOne())
{
return true;
}
return false;
}
Côté template:
// apps/myApp/modules/article/templates/editSuccess.php
...
<tr>
<td>Categories: </td>
<td>
<table>
<?php
foreach(Category::getAllCategories() as $category)
{
$categoryRender = array();
// On test pour chaque catégorie si elle est liée à l'article
if ($category->isACurrentCategoryOfArticle($article))
{
$categoryRender = array("checked" => "checked");
}
echo "<tr><td>".$form["category"][$category->getId()]->renderError().$form["category"][$category->getId()]->renderLabel()."</td>";
echo "<td>";
echo $form["category"][$category->getId()]->render($categoryRender);
echo "</td></tr>";
}
?>
</table>
</td>
</tr>
...
L’affichage est prêt, il ne reste qu’à sauvegarder les catégories liées. Pour cela, on test pour chaque catégorie si elle est cochée. Si oui, on test si elle n’existe pas déjà, et on enregistre un nouvel objet Article_category. Sinon, on regarde si elle existe, et pour délier l’article d’une catégorie, on supprime l’objet Article_category déjà existant.
Actions:
// apps/myApp/modules/article/actions/actions.class.php
...
protected function processForm(sfWebRequest $request, sfForm $form)
{
$form->bind($request->getParameter($form->getName()), $request->getFiles($form->getName()));
$values = $form->getValues();
if ($form->isValid())
{
$article = $form->save();
foreach (Category::getAllCategories() as $category)
{
// Si l'id est une clef dans le tableau récupéré après l'envoie
// (Ça nous permet de tester si la donnée correspond à une catégorie existante)
if (array_key_exists($category->getId(), $values["category"]))
{
// Si la catégorie est cochée
if ($values["category"][$category->getId()] == true)
{
// Si un objet Article_category n'existe pas déjà, on l'a joint à l'article
if (!$category->isACurrentCategoryOfArticle($article))
{
$articleCategory = new Article_category();
$articleCategory->setArticleId($article->getId());
$articleCategory->setCategoryId($category->getId());
$articleCategory->save();
}
}
else
{
// Si la catégorie n'est pas cochée, on supprime l'objet Article_category correspondant si l'article était liée à cette dernière
if ($category->isACurrentCategoryOfArticle($article))
{
$articleCategory = Article_category::getArticleCategory($article->getId(), $category->getId());
$articleCategory->delete();
}
}
}
}
$this->redirect($this->generateUrl('article_edit',$article));
}
}
Ça marche très bien, mais la logique voudrait que cela se passe dans la class du formulaire pour une réutilisation plus aisée !
Pour cela, il faudrait d’abord que l’on obtienne l’id de l’article juste après la sauvegarde du formulaire puis que l’on créé nos Article_category juste après. Impossible avec Symfony, du moins je n’ai pas trouvé.
Bref, mon bout de code restera dans l’action… sauf si vous avez une meilleure idée ? :p
Il suffit de créer un bon schema.yml (effectuer quelque modifications à la main) pour que symfony gère tout automatiquement. J’ai rencontré le même problème car je créais mon schema.yml à partir de la base de données avec « symfony doctrine:build-schema », ce qui n’est pas très propre finalement.
Donc pour les relations Many-to-Many je vous conseille de modifier votre schema.yml exactement comme indiqué dans l’exemple ci-dessous (sinon ça ne fonctionne pas):
http://www.symfony-project.org/jobeet/1_4/Doctrine/fr/03
Voila et bonne chance ++
En effet merci pour l’info je n’avais pas pensé à transformer mes champs en clé primaire ^^ !
Merci c’est exactement ce que je cherchais !
En revanche je n’ai pas compris vos deux commentaires. Comment transposer ce code dans la classe du formulaire ?