[TUTO][1.3, 1.4] Listes liées ou listes dépendantes
Bonjour à tous,
Bon étant donné qu'on a eu ces derniers jours plusieurs fois la question "Comment on fait pour faire des select box dépendantes l'un de l'autre en symfony ?", je fais un rapide tuto. Et vous allez voir que même si on code avec symfony, ça n'a rien de sorcier.
Avertissement : Je ne prétends absolument pas que mon post présente la meilleure des solutions. C'est un tuto basique, fonctionnel, qui devrait pouvoir fournir à ceux qui ont du mal une base afin d'implémenter cette fonctionnalité. Je n'ai sûrement pas gérer tous les cas possibles, les erreurs possibles, je n'ai pas utilisé de sfForm ce sont juste deux selects basiques mais le principe reste le même avec un sfForm. De plus, ce qui est fait au niveau du modèle est sûrement pas ce qu'il y a de mieux, mais ce n'est pas le sujet. (Le findAll() c'est maaaaaaaaaaaaaal)
Toutes les remarques et commentaires afin d'améliorer l'exemple sont les bienvenus, mais je répète que celui-ci n'a pas pour but de gérer tous les trucs possibles et imaginables pour ce genre de cas. Les questions sont aussi les bienvenues.
Contexte : Aller on va prendre un sujet un peu sympathique, vous imaginez qu'on est dans un jeu, qu'on a des personnages, qui possèdent un ensemble d'objets dans leur inventaire. Et on va faire justement l'écran qui va nous permettre d'utiliser ces objets.
C'est parti !
Pour commencer, je vous laisse le soin d'installer symfony, d'installer votre projet, de générer une application (que j'appellerai frontend pendant ce tuto). D'avoir votre serveur apache configuré et avoir la page par défaut de symfony. Une base de donnée disponible aussi, avec le database.yml qui va bien.
Bon déjà, de quoi va-t-on avoir besoin ? On veut deux listes en ajax (non, pas le produit ménager ...), il nous faut de quoi faire de l'ajax. On va donc commencer par récupérer jQuery. Oui, c'est un choix tout à fait arbitraire, mais c'est comme ça. Et non, je ne rédigerai pas une version du tuto avec un autre framework javascript. :P
Code:
./symfony plugin:install sfJqueryReloadedPlugin
Bon il me faut aussi un model correct pour travailler. Beaucoup d'entre-vous aurons le leur mais je mets le miens au cas où certaines personnes veuillent faire le tuto pas à pas.
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| # /config/doctrine/schema.yml
MainCharacter:
columns:
id: { type: integer(4), unsigned: true, notnull: true, primary: true, autoincrement: true }
name: { type: string(50), notnull: true }
relations:
Items:
class: Item
refClass: MainCharacterItem
local: main_character_id
foreign: item_id
foreignAlias: Characters
Item:
columns:
id: { type: integer(4), unsigned: true, notnull: true, primary: true, autoincrement: true }
name: { type: string(50), notnull: true }
MainCharacterItem:
columns:
main_character_id: { type: integer(4), unsigned: true, notnull: true, primary: true }
item_id: { type: integer(4), unsigned: true, notnull: true, primary: true }
quantity: {type: integer(4), unsigned: true, notnull: true }
relations:
MainCharacter:
local: main_character_id
foreign: id
foreignAlias: MainCharacterItems
onDelete: CASCADE
Item:
local: item_id
foreign: id
foreignAlias: MainCharacterItems
onDelete: CASCADE |
On va avoir besoin d'avoir aussi des données pour travailler, voici donc le fixtures.yml pour ceux qui en auraient besoin :
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| # /data/fixtures.yml
MainCharacter:
lightning:
name: Lightning
serah:
name: Serah
vanille:
name: Vanille
Item:
potion:
name: Potion
queue_de_phenix:
name: Queue de Phénix
elixir:
name: Elixir
antidote:
name: Antidote
collyre:
name: Collyre
MainCharacterItem:
potion_lightning:
MainCharacter: lightning
Item: potion
quantity: 5
potion_serah:
MainCharacter: serah
Item: potion
quantity: 2
elixir_serah:
MainCharacter: serah
Item: elixir
quantity: 3
queue_de_phenix_vanille:
MainCharacter: vanille
Item: queue_de_phenix
quantity: 2
antidote_vanille:
MainCharacter: vanille
Item: antidote
quantity: 4
collyre_vanille:
MainCharacter: vanille
Item: collyre
quantity: 1 |
Donc vous êtes censés avoir votre application qui fonctionne, on va avoir besoin d'un module maintenant :
Code:
./symfony generate:module frontend inventory
On fait une petite route de base, obligatoire puisque vous avez supprimé les routes par défaut vu que vous suivez les recommandations :roll: :
Code:
1 2 3 4
| # /apps/frontend/config/routing.yml
inventory_show:
url: /inventory.:sf_format
param: { module: inventory, action: index, sf_format: html } |
Alors, quand on va arriver sur l'écran de l'inventaire, on aura déjà une liste prête avec le nom des personnages. Notre petite action est donc très simple :
Code:
1 2 3 4 5 6 7 8
| // /apps/frontend/modules/inventory/actions/actions.class.php
class inventoryActions extends sfActions
{
public function executeIndex(sfWebRequest $request)
{
$this->characters = Doctrine::getTable('MainCharacter')->findAll();
}
} |
Le template qui va avec :
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
<!-- /apps/frontend/modules/inventory/templates/indexSuccess.php -->
<?php use_javascript('/sfJqueryReloadedPlugin/js/jquery-1.3.2.min.js'); ?>
<h1>Inventory</h1>
<select name="character_id" id="characters">
<option>Please select a character</option>
<?php foreach ($characters as $character): ?>
<option value="<?php echo $character->getId() ?>"><?php echo $character->getName(); ?></option>
<?php endforeach; ?>
</select>
<select name="item_id" id="items"></select>
<input type="submit" value="Use this item !" />
<script type="text/javascript">
$(document).ready(function() {
$("#characters").change(function(event) {
var characterId = $(event.target).val();
$.post('<?php echo url_for('@inventory_update_items_list') ?>', { character_id: characterId }, function(result) {
$("#items").html(result);
});
});
});
</script> |
C'est peu commenté je sais, mais ça ne devrait pas être bien dur à comprendre. Ici on affiche notre première liste avec les personnages, et on affiche une deuxième liste vide. Ensuite il y a du javascript. Je vous suggère d'apprendre jQuery si vous y comprenez rien :lol:, mais en gros, quand la page est chargée, on surveille l'évènement "onChange" de notre liste de personnage pour faire une requête ajax dès que la valeur sélectionnée change.
On récupère donc la valeur actuellement selectionnée, puis on la post vers la route @inventory_update_items_list
Là dans vos têtes ça doit faire "Ding ! On a une nouvelle route" (Attention, si ça ding trop fort c'est que votre tête est vide). Et ben il reste plus qu'à la rajouter dans le routing.yml :
Code:
1 2 3 4 5 6
| # /apps/frontend/config/routing.yml
inventory_update_items_list:
url: /inventory/update-items-list.:sf_format
param: { module: inventory, action: updateItemsList, sf_format: html }
requirements:
sf_method: post |
Ah, maintenant on a donc une nouvelle action qui va se charger de récupérer les éléments de la deuxième liste. Allons-y alors :
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // apps/frontend/modules/inventory/actions/actions.class.php
public function executeUpdateItemsList(sfWebRequest $request)
{
if (!$request->isXmlHttpRequest())
{
return sfView::NONE;
}
$this->character = Doctrine::getTable('MainCharacter')->find($request->getParameter('character_id'));
if (!$this->character)
{
return sfView::NONE;
}
} |
Rien de sorcier là encore : Si ce n'est pas une requête ajax, on renvoit une page blanche. Bon à la place vous auriez pu faire une redirection, une 404, ou ce que vous voulez peu importe. Ensuite on récupère le personnage concerné grâce au paramètre transmis par post.
Dans le cas où on ne le trouve pas, là encore j'ai choisi d'afficher une page blanche.
Le template qui va avec cette action :
Code:
1 2 3 4 5
|
<!-- /apps/frontend/modules/inventory/templates/updateItemsListSuccess.php -->
<?php foreach ($character->getMainCharacterItems() as $mainCharacterItem): ?>
<option value="<?php echo $mainCharacterItem->getItem()->getId() ?>"><?php echo $mainCharacterItem; ?></option>
<?php endforeach; ?> |
Alors là, j'ai fait le rendu des options de ma 2ème liste en html directement. Vous pouvez soit faire pareil, soit par exemple faire un rendu en json et utiliser le javascript après le retour de votre requête ajax pour construire les différentes options. Bon, le plus simple je trouve c'est de faire le html directement, j'ai choisi ça par soucis de rapidité à faire. Et si vous avez rien compris à ce que je viens de dire, faites comme moi :ccool:
Donc là, toujours rien de sorcier. Cette page va afficher une liste de tags <option> qui contiendront l'id de l'objet en value, et qui en texte affichent directement l'objet MainCharacterItem. MainCharacterItem ? Mais il n'y a que 3 colonnes de type entier dedans. Diantre ! Pourquoi lui ? Alors en effet, j'aurais pu afficher directement le nom de l'objet, mais dans MainCharacterItem on a aussi une donnée qui peut être intéressante : La quantité possédée par le personnage ! Donc on va se faire la petite fonction __toString() qui va bien dans le model :
Code:
1 2 3 4 5 6 7 8
| // /lib/model/doctrine/MainCharacterItem.class.php
class MainCharacterItem extends BaseMainCharacterItem
{
public function __toString()
{
return $this->getItem()->getName()." (".$this->getQuantity().")";
}
} |
(Oui, comme vous vous en doutez, ça n'a rien à voir avec le tuto :mrgreen:. Mais c'est moi qui rédige, donc "je fais qu'est ce que je veux" comme dirait mon petit cousin)
Et voilà, votre requête ajax renvoit donc du html qui est inséré directement à l'intérieur de la seconde liste.
Voilà, c'est fini. Au final je me rend compte que c'était tellement pas compliqué que peut-être que personne va apprendre quoi que ce soit avec ce truc :mrgreen:.
En tout cas, y'a rien de différent parce qu'on est en symfony. La seule chose qui diffère, c'est où vous mettez votre code qui gère la requête ajax en gros.