Controler l’attribut ‘disabled’ d’un sfWidgetFormDoctrineChoice
Dans symfony, le widget sfWidgetDoctrineChoice permet de générer des tags <select>, <input type=’checkbox’> ou <input type=’radio’> a partir d’enregistrement dans la base de donnée.
Nous allons voir dans ce billet comment désactiver certains de ces choix dynamiquement dans le cas d’un tag <select> en contrôlant l’attribut ‘disabled’. J’ai mis cette méthode en application dans le cadre d’un outil de mailing où je ne souhaite pas qu’un client puisse faire un envoi sur une liste qui n’a pas fini d’être importée (l’import se fait en tache de fond et peut durer longtemps, j’expliquerai cette technique dans un autre article).
On pourrais se contenter de lister uniquement les listes dont les imports sont terminés mais l’utilisateur se demandera alors ou sont passé ses listes en cours d’importation. Une autre solution de facilité serait de faire une validation une fois le formulaire soumis en affichant une erreur si une des mauvaises listes a été choisie, pas très user friendly…
La technique expliquée ici semble être ‘inédite’, je n’ai pas trouvé de documentation expliquant cette façon de faire et les questions posées sur le sujet dans les forums et les listes étaient sans réponses ou avec des réponses incomplète, j’espère donc que cet article aidera quelques codeurs.
Commençons par la mise en place du widget dans le formulaire et changer les options par défaut du sfWidgetFormDoctrineChoice:
$this->widgetSchema['list_id'] = new sfWidgetFormDoctrineChoice( array( 'model' => 'AddressListTable', 'add_empty' => 'Select a list', 'query' => AddressListTable::getForProfile($profile), 'method' => 'getWithStatus', 'renderer_class' => 'sfWidgetFormSelectAcid' ), array() );
Passons en revue les différentes options du widget :
- model n’est pas utile dans notre cas, mais il est obligatoire donc on le laisse.
- add_empty rajoute une option par défaut avec une valeur nulle dont le texte sera ‘Select a list’
- query sert a récupérer les objets qui peupleront le widget, ici rien d’exceptionnel c’est une requête qui filtre les listes par profil. Notez bien le nom : ‘query’ et renvoyez bien un Doctrine_Query et pas un Doctrine_Collection
- method, par défaut symfony utilise la méthode __toString() de l’objet en question mais ici nous allons nous servir de cette option pour récupérer davantage de données
- renderer_class : c’est ici que se passe le travail et où nous allons manipuler l’attribut ‘disabled’.
Nous allons voir maintenant la méthode getWithStatus() dans la classe AddressList :
public function getWithStatus() { $disabled = ($this->getImport()->getJobStatus()) ? true : false; $working = ($this->getImport()->getJobStatus()) ? " (un import est en cours, la liste n'est peut être pas complête)" : ""; return array( 'label' => $this->name . $working, 'disabled' => $disabled ); }
On ne renvoie plus une chaîne comme ce qui est attendu mais un tableau avec les clés ‘label’ et ‘disabled’. Dans le cas ou on passe l’option a disabled, on affiche la raison dans le label.
Passons a la dernière étape, la classe sfWidgetFormSelectAcid (Pourquoi le nom de cette classe d’appelle t’elle sfWidgetFormSelectAcid ? Je vous laisse le découvrir :D). Symfony offre quelques classes de base dérivées de sfWidgetFormChoiceBase : sfWidgetFormSelectCheckbox pour une liste de cases a cocher, sfWidgetFormSelectMany pour un select a choix multiples, sfWidgetFormSelectRadio pour les boutons radios et sfWidgetFormSelect qui est celui qui nous intéresse dans le cas présent. sfWidgetFormSelectAcid va donc étendre cette classe :
class sfWidgetFormSelectAcid extends sfWidgetFormSelect { /** * Returns an array of option tags for the given choices * * @param string $value The selected value * @param array $choices An array of choices * * @return array An array of option tags */ protected function getOptionsForSelect($value, $choices) { $mainAttributes = $this->attributes; $this->attributes = array(); if (!is_array($value)) { $value = array($value); } $value = array_map('strval', array_values($value)); $value_set = array_flip($value); $options = array(); foreach ($choices as $key => $option) { $attributes = array('value' => self::escapeOnce($key)); if (isset($value_set[strval($key)])) { $attributes['selected'] = 'selected'; } if (is_array($option)) { $label = $option['label']; $attributes['disabled'] = $option['disabled']; } else { $label = $option; } $options[] = $this->renderContentTag('option', self::escapeOnce($label), $attributes); } $this->attributes = $mainAttributes; return $options; } }
De la classe de base, nous n’avons surchargé qu’une seule méthode getOptionsForSelect qui s’occupe justement de générer les tags option. La plus grande partie est une copie directe de la méthode de base et la partie qui nous intéresse vraiment commence a la ligne
if (is_array($option)) {
. On prends toujours en compte le cas ou l’option est une chaine de caractère pour prendre en charge l’option ‘add_empty’ du widget et dans le cas d’un tableau, on construit le tag avec les données renvoyées par le modèle.
Le widget spécialement conçu pour désactiver des options a partir d’états dans la base de donnée est maintenant opérationnel. Le résultat sera utile dans de très rares occasion mais c’est surtout la méthode utilisée pour y arriver qui a été enrichissante : La documentation montrant ses limites, il faut apprendre a connaitre le framework en plongeant dans son code, les docstrings et un IDE tel que Netbeans 6.9 aidant beaucoup.