Techniques Avancées de Routage
- Initialisation du Projet : Un CMS pour Plusieurs Clients
- Mécanismes Internes du Framework de Routage
- Créer une Classe de Route Personnalisée
- Les Collections de Routes
- Créer une Classe Personnalisée de Collection de Routes
- Personnaliser la Collection de Routes d'Objet
- Options d'une Collection de Routes
- Conclusion
Par Ryan Weaver
En son noyau, le framework de routage est en réalité une carte qui relie chaque URL à une destination spécifique à l'intérieur d'un projet symfony et vice versa. Cet outil permet de créer des URLs propres et élégantes, et qui restent complètement indépendantes de la logique applicative. Ces améliorations ont été réalisées depuis les versions récentes de symfony, et le framework de routing continue encore d'aller plus loin aujourd'hui.
Ce chapitre décrira comment créer une application web simple pour laquelle chaque client utilise un sous-domaine séparé, par exemple client1.mydomain.com
et client2.mydomain.com
. C'est en étendant le framework de routage que cette tâche devient très facile à réaliser.
note
Ce chapitre nécessite l'usage de Doctrine en guise de couche d'ORM pour le projet symfony.
Initialisation du Projet : Un CMS pour Plusieurs Clients
Dans ce chapitre, une société fictive - Sympal Builder - désire mettre en place un CMS permettant à ses clients de construire leur propre site web et l'héberger sur un sous-domaine de sympalbuilder.com
. L'objectif consiste ainsi à rendre le site grand public accessible à l'adresse xxx.sympalbuilder.com
pour le client XXX, et l'interface d'administration correspondante à l'adresse xxx.sympalbuilder.com/backend.php
.
note
Le nom Sympal
est emprunté du nom de l'application
Sympal développée par Jonathan Wage. Sympal est un
framework de gestion de contenu bâti sur un socle technique symfony et
Doctrine.
Ce projet dispose de deux besoins fonctionnels basiques et obligatoires :
Les utilisateurs doivent être capables de créer des pages web et spécifier pour chacune d'entre elles un titre, un contenu et une URL correspondante.
L'application entière doit être construite à l'intérieur d'un seul projet
symfony qui gère à la fois les applicationsfrontend
etbackend
de tous les clients du site. La détermination du client est basée sur le sous-domaine demandé et afin de permettre le chargement des bonnes informations dans l'application.
note
Pour créer cette application, le serveur web aura besoin de rediriger toutes
les requêtes des sous-domaines *.sympalbuilder.com
vers le même document
racine, le répertoire web du projet symfony.
Le Modèle de Données et les Données
La base de données du projet se compose de deux tables Client
et Page
. Chaque Client
est représenté par un site (et donc un sous-domaine) qui contient plusieurs objets de type Page
.
# config/doctrine/schema.yml Client: columns: name: string(255) subdomain: string(50) indexes: subdomain_index: fields: [subdomain] type: unique Page: columns: title: string(255) slug: string(255) content: clob client_id: integer relations: Client: alias: Client foreignAlias: Pages onDelete: CASCADE indexes: slug_index: fields: [slug, client_id] type: unique
note
Bien que les indexes sur chaque table ne soient pas nécessaires, il est tout de même important de les définir dans la mesure où la base de données sera fréquemment interrogée sur ces colonnes.
Pour commencer à donner naissance au projet, il est nécessaire d'avoir dès le départ quelques jeux de données de test dans le fichier data/fixtures/fixtures.yml
:
# data/fixtures/fixtures.yml Client: client_pete: name: Pete's Pet Shop subdomain: pete client_pub: name: City Pub and Grill subdomain: citypub Page: page_pete_location_hours: title: Location and Hours | Pete's Pet Shop content: We're open Mon - Sat, 8 am - 7pm slug: location Client: client_pete page_pub_menu: title: City Pub And Grill | Menu content: Our menu consists of fish, Steak, salads, and more. slug: menu Client: client_pub
Ce jeu de données de test déclare initialement deux sites web constitués chacun d'une seule page. L'url complète vers chaque page est définie à l'aide des deux colonnes subdomain
et slug
de l'objet de modèle Page
.
http://pete.sympalbuilder.com/location http://citypub.sympalbuilder.com/menu
Le Routage
Chaque page d'un site Sympal Builder correspond directement à un objet de modèle Page
pour lequel sont définis un titre et un contenu à afficher. L'étape suivante consiste à relier une URL à un objet Page
de l'application. Pour ce faire, il suffit de créer une route d'objet de type sfDoctrineRoute
qui s'appuie sur la colonne slug
. Ainsi, le code suivant recherchera automatiquement un objet Page
dans la base de données, pour lequel la valeur du champ slug
correspond à celui transmis dans l'url :
# apps/frontend/config/routing.yml page_show: url: /:slug class: sfDoctrineRoute options: model: Page type: object params: module: page action: show
La route ci-dessus correspondra automatiquement à l'url http://pete.sympalbuilder.com/location
ainsi qu'à son objet Page
associé. Malheureusement, cette route correspondra aussi à l'url http://pete.sympalbuilder.com/menu
, ce qui signifie que le menu du restaurant sera affiché sur le site de Pete ! Par conséquent, la route est encore incapable de faire la différence entre les sous-domaines des différents clients.
Pour concrétiser cette application, la route a besoin d'être plus intelligente. Elle devrait en effet correspondre à une Page
en se basant à la fois sur la valeur de la colonne slug
et sur celle de la colonne client_id
. En d'autres termes, il s'agit de faire correspondre l'hôte (par exemple pete.sympalbuilder.com
) avec la colonne subdomain
du modèle Client
. Pour y parvenir, il suffit d'étendre le framework de routage en créant une classe personnalisée de route. Néanmoins, avant de démarrer, il convient de rappeler quelques principes de base au sujet du fonctionnement du système de routage de symfony.
Mécanismes Internes du Framework de Routage
Dans symfony, une route est un objet de type sfRoute
qui dispose de deux rôles importants :
Générer une URL : par exemple, si un paramètre
slug
est passé à une règle
page_show
, alors la route doit être capable de générer une véritable URL correspondante (/location
par exemple).Reconnaître une URL entrante : en lui fournissant l'url d'une requête entrante, chaque route doit être capable de déterminer si l'URL correspond aux contraintes de la route.
Les informations de chaque route individuelle sont généralement déclarées à l'intérieur de chaque répertoire config/
d'une application, et plus précisément dans le fichier app/yourappname/config/routing.yml
. Il est important de se rappeler que chaque route est un objet de type sfRoute
. Par conséquent, comment ces quelques données YAML simples deviennent de véritables objets sfRoute
?
Le Gestionnaire de Configuration du Cache du Routage
En dépit du fait que la plupart des routes sont définies dans un fichier YAML, chaque entrée de ce fichier est en réalité transformée en un objet au traitement de la requête.
Cette transformation est assurée à l'aide d'une classe spéciale plus communément intitulée gestionnaire de configuration de cache. A l'issue de la conversion, il en résulte du code PHP représentant toutes les routes de l'application. Bien que les spécificités de ce mécanisme dépassent le périmètre de ce chapitre, il n'en demeure pas moins intéressant de tirer quelques informations de la version compilée de la route page_show
.
Le code compilé se trouve en effet dans le fichier cache/yourappname/envname/config/config_routing.yml.php
pour chaque application et pour chaque environnement. Le listing ci-dessous est une version simplifiée de ce à quoi ressemble la route page_show
:
new sfDoctrineRoute('/:slug', array ( 'module' => 'page', 'action' => 'show', ), array ( 'slug' => '[^/\\.]+', ), array ( 'model' => 'Page', 'type' => 'object', ));
tip
Le nom de la classe de chaque route est défini grâce à la clé class
du
fichier routing.yml
. Si aucune clé class
n'est spécifiée, la route par
défaut sera issue de la classe sfRoute
. Une autre classe spécifique plus
commune est sfRequestRoute
qui offre au développeur la possibilité de créer
des routes dites RESTful. Une liste complète des classes de route et des
options disponibles existe dans le guide de référence de symfony.
Faire Correspondre une Requête Entrante à une Route Spécifique
L'un des principaux rôle du framework de routage consiste à faire correspondre chaque URL entrante avec le bon objet route. C'est la classe sfPatternRouting
qui représente le moteur du noyau du routage et qui est dédiée à cette tâche. En dépit de son importance, un développeur n'interagira que très rarement avec sfPatternRouting
.
Pour faire correspondre la bonne route, la classe sfPatternRouting
itère sur chaque objet sfRoute
et demande à la route si elle répond à l'url entrante. Intérieurement, cela signifie que sfPatternRouting
appelle la méthode sfMethod::matchesUrl()
sur chaque objet route. Cette méthode retourne simplement false
si la route ne correspond pas à l'url entrante.
A contrario, si la route répond parfaitement à l'URL entrante, la méthode sfRoute::matchesUrl()
ne renverra pas seulement la valeur true
. Bien au contraire, la route retourne un tableau des paramètres qui sont ensuite fusionnés à l'intérieur de l'objet représentant la requête. Par exemple, l'url http://pete.sympalbuilder.com/location
répond à la route page_show
, dont la méthode matchesUrl()
retourne alors le tableau suivant :
array('slug' => 'location')
Cette information est ensuite fusionnée dans l'objet de requête, et c'est pourquoi les paramètres de la route (slug
par exemple) sont accessibles depuis le fichier d'actions.
$this->slug = $request->getParameter('slug');
Comme on peut s'en douter, surcharger ou bien redéfinir complètement la méthode sfRoute::matchesUrl()
est un excellent moyen d'étendre et de personnaliser une route afin de réaliser presque tout ce que l'on souhaite.
Créer une Classe de Route Personnalisée
L'étape suivante consiste à présent à créer une nouvelle classe de route personnalisée dans le but d'étendre la route page_show
actuelle afin qu'elle puisse se baser sur le sous-domaine des objets Client
. Pour ce faire, il suffit de créer un nouveau fichier nommé acClientObjectRoute.class.php
et de le placer dans le répertoire lib/routing
du projet en ayant pris le soin de créer ce répertoire juste avant :
// lib/routing/acClientObjectRoute.class.php class acClientObjectRoute extends sfDoctrineRoute { public function matchesUrl($url, $context = array()) { if (false === $parameters = parent::matchesUrl($url, $context)) { return false; } return $parameters; } }
Après avoir vidé le cache de symfony, il ne reste plus qu'à indiquer à la route page_show
d'utiliser cette classe de route en modifiant la valeur de la clé class
de la définition de la route dans le fichier routing.yml
:
# apps/fo/config/routing.yml page_show: url: /:slug class: acClientObjectRoute options: model: Page type: object params: module: page action: show
Pour l'instant, la classe acClientObjectRoute
n'apporte aucune fonctionnalité supplémentaire mais toutes les pièces du puzzle sont désormais en place. La section suivante explique quels sont les deux rôles spécifiques de la méthode matchesUrl()
et donne la démarche à suivre pas à pas pour redéfinir la logique de cette dernière.
Ajouter de la Logique à la Route Personnalisée
Pour ajouter la fonctionnalité requise à la classe de route personnalisée, il suffit de remplacer le contenu du fichier acClientObjectRoute.class.php
par le code ci-dessous.
class acClientObjectRoute extends sfDoctrineRoute { protected $baseHost = '.sympalbuilder.com'; public function matchesUrl($url, $context = array()) { if (false === $parameters = parent::matchesUrl($url, $context)) { return false; } // return false if the baseHost isn't found if (strpos($context['host'], $this->baseHost) === false) { return false; } $subdomain = str_replace($this->baseHost, '', $context['host']); $client = Doctrine_Core::getTable('Client') ->findOneBySubdomain($subdomain) ; if (!$client) { return false; } return array_merge(array('client_id' => $client->id), $parameters); } }
Le premier appel à la méthode parent::matchesUrl()
est important puisqu'il exécute le processus classique d'analyse de la route. Dans cet exemple, tant que l'URL /location
répond à la route page_show
, la méthode parent::matchesUrl()
doit retourner un tableau contenant le paramètre slug
correspondant.
array('slug' => 'location')
En d'autres termes, tout le travail complexe d'analyse de la route a été réalisé par le framework, ce qui permet au reste de la méthode de se concentrer sur l'analyse de la correspondance avec le sous-domaine du Client
.
public function matchesUrl($url, $context = array()) { // ... $subdomain = str_replace($this->baseHost, '', $context['host']); $client = Doctrine_Core::getTable('Client') ->findOneBySubdomain($subdomain) ; if (!$client) { return false; } return array_merge(array('client_id' => $client->id), $parameters); }
En exécutant un simple remplacement de chaine, il est possible d'isoler la portion de l'hôte issu du sous-domaine, afin d'interroger ensuite la base de données pour vérifier si un objet Client
a ce sous-domaine. Si aucun client ne possède ce sous-domaine, alors la méthode retourne false
afin d'indiquer que la requête entrante ne correspond pas à la route.
En revanche, si un objet Client
répondant au sous-domaine courant existe dans la base de données, alors un tableau contenant un paramètre supplémentaire client_id
est fusionné avec le tableau original retourné.
tip
Le tableau $context
passé à la méthode matchesUrl()
est prérempli
d'informations utiles concernant la requête courante, incluant l'hôte (host
),
un booléen is_secure
, le request_uri
, la méthode (method
) HTTP et bien
plus encore.
Ceci étant fait, on peut se demander ce que la nouvelle classe de route personnalisée a vraiment accompli. La classe acClientObjectRoute
réalise désormais les tâches suivantes :
La variable entrante
$url
correspondra à la route uniquement si l'hôte contient un sous-domaine appartenant à l'un des objetsClient
du modèle.Si la route répond, alors un paramètre additionnel
client_id
pour l'objet de modèle correspondant est retourné dans le tableau, qui sera ensuite fusionné dans les paramètres de la requête.
Profiter de la Route Personnalisée
Maintenant que le bon paramètre client_id
est retourné par la classe acClientObjectRoute
, il devient alors naturellement accessible via l'objet de la requête. Par exemple, l'action page/show
peut ainsi utiliser le paramètre client_id
pour retrouver l'objet Page
correct :
public function executeShow(sfWebRequest $request) { $this->page = Doctrine_Core::getTable('Page')->findOneBySlugAndClientId( $request->getParameter('slug'), $request->getParameter('client_id') ); $this->forward404Unless($this->page); }
note
La méthode findOneBySlugAndClientId()
est un nouveau type de finders magiques introduit dans Doctrine 1.2 qui permet d'interroger
une table en s'appuyant sur plusieurs champs.
Aussi simple que cela puisse paraître, le framework de routage propose une solution encore plus élégante. Il suffit dans un premier temps d'ajouter la méthode ci-dessous à la classe acClientObjectRoute
.
protected function getRealVariables() { return array_merge(array('client_id'), parent::getRealVariables()); }
Avec cette dernière pièce du puzzle, l'action peut s'appuyer entièrement sur la route pour retourner un objet Page
correspondant. L'action page/show
peut ainsi être réduite à seulement une ligne.
public function executeShow(sfWebRequest $request) { $this->page = $this->getRoute()->getObject(); }
Sans aucun effort additionnel, le code ci-dessus interrogera la base de données pour récupérer un objet Page
en s'appuyant sur les deux colonnes slug
et client_id
. De plus, comme toutes les routes d'objet, l'action est automatiquement redirigée vers une page d'erreur 404 si aucun objet correspondant n'a été trouvé.
Mais comment tout cela fonctionne-t-il ? Les routes d'objet comme sfDoctrineRoute
, dont la classe acClientObjectRoute
hérite, interrogent automatiquement la base de données afin de récupérer l'objet correspondant aux valeurs des variables spécifiées à la clé url
de la route. Par exemple, la route page_show
, qui contient la variable slug
dans son url
, demande à la base de données de lui donner l'objet Page
en s'aidant de la colonne slug
.
Or dans cette application, la route page_show
doit également s'appuyer sur la valeur de la colonne client_id
afin de récupérer les objets correspondants. Pour ce faire, il aura seulement fallu surcharger la méthode sfObjectRoute::getRealVariables()
qui est automatiquement appelée à l'intérieur de l'objet pour déterminer sur quelles colonnes la requête SQL doit être exécutée. En ajoutant le champ client_id
au tableau, l'objet acClientObjectRoute
interrogera la base de données en se basant sur ces deux colonnes slug
et client_id
.
note
Les routes d'objets ignorent automatiquement toutes les variables qui ne
correspondent pas à des colonnes réelles. Par exemple, si la clé url
contient
une variable :page
tandis qu'aucune colonne page
n'existe dans la table
associée, alors cette variable sera ignorée.
En l'état actuel des choses, la classe de route personnalisée accomplit tout ce qui est nécessaire sans trop d'effort. Cette nouvelle route sera réutilisée dans les prochaines sections pour créer une interface d'administration spécifique à chaque client.
Générer la Bonne Route
Un seul petit problème subsiste avec la manière dont est générée l'url. Pour comprendre, il suffit de regarder l'exemple suivant qui décrit la création d'un lien vers une page.
<?php echo link_to('Locations', 'page_show', $page) ?>
Url générée: /location?client_id=1
Comme on peut le constater, le paramètre client_id
a été automatiquement ajouté à la fin de l'url. Cela se produit en effet parce que la route essaie d'utiliser toutes les variables pour générer l'url. Comme la route sait qu'elle doit utiliser les deux paramètres slug
et client_id
, alors elle les ajoute à l'url qu'elle génère. Pour fixer cette petite contrariété, il suffit d'ajouter la méthode suivante à la classe acClientObjectRoute
.
protected function doConvertObjectToArray($object) { $parameters = parent::doConvertObjectToArray($object); unset($parameters['client_id']); return $parameters; }
Lorsqu'une route d'objet est générée, elle s'attend à retrouver toutes les informations nécessaires en appelant la méthode doConvertObjectToArray()
. Par défaut, le paramètre client_id
est retourné dans le tableau $parameters
. Désormais, en supprimant ce paramètre du tableau, cela permet ainsi d'éviter de le voir réapparaître dans l'url générée. Il est important de se souvenir que la route d'objet peut s'offrir ce luxe dans la mesure où l'information du Client
est contenue dans le sous-domaine lui-même.
tip
Le traitement de la méthode doConvertObjectToArray()
peut entièrement être
redéfini et géré par les soins du développeur en ajoutant une méthode
toParams()
à la classe de modèle. Cette méthode se doit de retourner un
tableau des paramètres qui doivent figurer au moment de la génération de la
route.
Les Collections de Routes
Pour en finir avec l'application Sympal Builder, une interface d'administration doit être créée dans laquelle chaque Client
individuel sera capable de gérer ses propres Pages
. Pour ce faire, l'application a besoin d'un jeu d'actions pour lister, créer, éditer et supprimer des objets Page
.
Comme tous ces types de modules sont sensiblement génériques, symfony est capable de les générer automatiquement. Il suffit pour ce faire d'exécuter la tâche suivante depuis la ligne de commande afin de générer un module pageAdmin
à l'intérieur d'une application backend
qui aura été créée juste avant.
$ php symfony doctrine:generate-module backend pageAdmin Page --with-doctrine-route --with-show
La tâche ci-dessus génère un module avec un fichier d'actions et ses vues associées capables de réaliser toutes les modifications nécessaires sur n'importe quel objet Page
. De nombreuses personnalisations de ce module CRUD généré peuvent être réalisées mais cela sort du cadre de ce chapitre.
Alors que la commande ci-dessus prépare le module pour le développeur, il reste encore une route à créer pour chaque action. En passant l'option --with-doctrine-route
à la commande, chaque action a été générée pour fonctionner avec une route d'objet. Cela réduit considérablement la taille du code dans chaque action. Par exemple, l'action edit
contient à présent seulement une simple ligne.
public function executeEdit(sfWebRequest $request) { $this->form = new PageForm($this->getRoute()->getObject()); }
Au total, l'application d'administration de Sympal Builder contient seulement les actions index
, new
, create
, edit
, update
, et delete
. Dans un schéma classique de développement, créer ces routes en vue d'une utilisation RESTful aurait obligé le développeur à davantage de configuration dans le fichier routing.yml
.
pageAdmin: url: /pages class: sfDoctrineRoute options: { model: Page, type: list } params: { module: page, action: index } requirements: sf_method: [get] pageAdmin_new: url: /pages/new class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: new } requirements: sf_method: [get] pageAdmin_create: url: /pages class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: create } requirements: sf_method: [post] pageAdmin_edit: url: /pages/:id/edit class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: edit } requirements: sf_method: [get] pageAdmin_update: url: /pages/:id class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: update } requirements: sf_method: [put] pageAdmin_delete: url: /pages/:id class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: delete } requirements: sf_method: [delete] pageAdmin_show: url: /pages/:id class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: show } requirements: sf_method: [get]
Enfin, pour connaître et visualiser la configuration de toutes les routes déclarées dans une application, il suffit d'exécuter la tâche app:routes
dans la console afin d'obtenir un résumé de la définition de chaque route.
$ php symfony app:routes backend >> app Current routes for application "backend" Name Method Pattern pageAdmin GET /pages pageAdmin_new GET /pages/new pageAdmin_create POST /pages pageAdmin_edit GET /pages/:id/edit pageAdmin_update PUT /pages/:id pageAdmin_delete DELETE /pages/:id pageAdmin_show GET /pages/:id
Remplacer les Routes par une Collection de Routes
Heureusement, symfony fournit une manière bien plus simple pour spécifier toutes les routes qui appartiennent à un CRUD traditionnel. Par conséquent, tout le contenu actuel du fichier routing.yml
se résume à une seule et unique route.
pageAdmin: class: sfDoctrineRouteCollection options: model: Page prefix_path: /pages module: pageAdmin
Une fois encore, l'exécution de la tâche app:routes
permet de visualiser toutes les routes définies. Comme on peut le constater, les sept routes précédentes existent toujours.
$ php symfony app:routes backend >> app Current routes for application "backend" Name Method Pattern pageAdmin GET /pages.:sf_format pageAdmin_new GET /pages/new.:sf_format pageAdmin_create POST /pages.:sf_format pageAdmin_edit GET /pages/:id/edit.:sf_format pageAdmin_update PUT /pages/:id.:sf_format pageAdmin_delete DELETE /pages/:id.:sf_format pageAdmin_show GET /pages/:id.:sf_format
Les collections de routes sont un type particulier de routes d'objet qui représentent intérieurement plus d'une seule route. La route sfDoctrineRouteCollection
, par exemple, génère automatiquement les sept routes les plus fréquentes nécessaires à un CRUD. En coulisses, la classe sfDoctrineRouteCollection
ne fait rien de plus que créer les sept même routes définies précédemment dans le fichier routing.yml
. Les collections de route existent de base comme raccourci pour créer un groupe de routes communes.
Créer une Classe Personnalisée de Collection de Routes
A partir de maintenant, chaque Client
sera capable de modifier ses propres objets Page
par l'intermédiaire d'un CRUD interne fonctionnel et accessible depuis l'URL /pages
. Malheureusement, chaque Client
peut pour le moment voir et modifier tous les objets Page
de la base de données. Par exemple, l'URL http://pete.sympalbuilder.com/backend.php/pages
génèrera une liste de deux objets Page
issus des jeux de données de test : la page location
de la boutique Pete's Pet Shop et le menu
de la page Menu
de City Pub.
Ce problème peut être corrigé en réutilisant la classe acClientObjectRoute
créée précédemment pour le frontend. La classe sfDoctrineRouteCollection
génère un groupe d'objets sfDoctrineRoute
mais il s'agit ici de générer un groupe d'objets acClientObjectRoute
à la place.
Pour y parvenir, il sera nécessaire d'avoir recours à une classe personnalisée de collection de routes. Il suffit alors de créer le nouveau fichier acClientObjectRouteCollection.class.php
dans le répertoire lib/routing
du projet avant de lui attribuer le contenu ci-dessous. On remarque au passage que ce code est particulièrement concis.
// lib/routing/acClientObjectRouteCollection.class.php class acClientObjectRouteCollection extends sfObjectRouteCollection { protected $routeClass = 'acClientObjectRoute'; }
La propriété $routeClass
définit la classe utilisée pour identifier chaque route sous-jacente. C'est tout ! Chaque route sous-jacente est désormais un objet de type acClientObjectRoute
. Ainsi, la page http://pete.sympalbuilder.com/backend.php/pages
affichera uniquement une seule page : la page location
du magasin animalier Pet's Pet Shop.
Grâce à cette nouvelle classe de route personnalisée, l'action index
retourne uniquement les objets Page
liés au Client
correspondant, en se basant toujours sur le sous-domaine de la requête. Avec seulement quelques lignes de code, un module entier d'administration des objets Page
a été créé, et ce celui-ci peut désormais être utilisé en toute sécurité par les multiples clients.
La Pièce Manquante : Créer une Nouvelle Page
Pour le moment, le formulaire de création ou d'édition d'une page affiche une liste déroulante de tous les objets Client
de la base de données. Pour des raisons évidentes de sécurité et d'ergonomie, il n'est pas question de laisser l'utilisateur choisir cette donnée.
Il s'agit donc à présent de découvrir comment affecter automatiquement l'objet Client
à la page en cours en se basant toujours sur le sous-domaine de la requête. Pour ce faire, il suffit de mettre à jour l'objet PageForm
situé dans le fichier lib/form/PageForm.class.php
.
public function configure() { $this->useFields(array( 'title', 'content', )); }
La liste déroulante a été retirée de tous les formulaires Page
comme cela était souhaité. Néanmoins, lorsque de nouveaux objets Page
sont créés, le champ client_id
reste quant à lui vide. Pour corriger ce défaut, il convient d'associer manuellement l'objet Client
correspondant à l'objet Page
dans les deux actions new
et create
.
public function executeNew(sfWebRequest $request) { $page = new Page(); $page->Client = $this->getRoute()->getClient(); $this->form = new PageForm($page); }
On remarque ici l'introduction d'une nouvelle méthode getClient()
qui n'existe pas encore dans la classe acClientObjectRoute
. Le code suivant présente l'implémentation de cette nouvelle méthode en réalisant seulement quelques modifications.
// lib/routing/acClientObjectRoute.class.php class acClientObjectRoute extends sfDoctrineRoute { // ... protected $client = null; public function matchesUrl($url, $context = array()) { // ... $this->client = $client; return array_merge(array('client_id' => $client->id), $parameters); } public function getClient() { return $this->client; } }
En ajoutant une propriété $client
à la classe et en la définissant dans la méthode matchesUrl()
, l'objet Client
correspondant peut alors facilement être rendu accessible grâce à la route. La colonne client_id
des nouveaux objets Page
sera automatiquement et correctement définie d'après le sous-domaine de l'hôte courant.
Personnaliser la Collection de Routes d'Objet
En utilisant le framework de routage, les problématiques soulevées par le cahier des charges de l'application Sympal Builder ont toutes été résolues. A mesure que l'application grandit, le développeur sera capable de réutiliser les routes personnalisées pour d'autres modules dans l'interface d'administration. Par exemple, chaque Client
pourra gérer ses propres galeries de photos.
Une autre raison récurrente qui encourage la création d'une collection de routes personnalisée est l'ajout de routes fréquemment utilisées. On peut par exemple imaginer un projet qui emploie plusieurs modèles, et que chacun d'eux dispose d'une colonne is_active
dans leur table respective. Dans l'interface d'administration, cela permettrait ainsi de définir une manière aisée pour activer la valeur de la colonne is_active
pour n'importe quel objet. Il s'agit donc tout d'abord de modifier la classe acClientObjectRouteCollection
et de lui indiquer d'ajouter une nouvelle route dans sa collection.
// lib/routing/acClientObjectRouteCollection.class.php protected function generateRoutes() { parent::generateRoutes(); if (isset($this->options['with_is_active']) && $this->options['with_is_active']) { $routeName = $this->options['name'].'_toggleActive'; $this->routes[$routeName] = $this->getRouteForToggleActive(); } }
La méthode sfObjectRouteCollection::generateRoutes()
est appelée lorsque la classe de collection de routes est instanciée, et est aussi responsable de la création de toutes les routes nécessaires et de leur ajout dans le tableau de la propriété $routes
. Dans ce cas, la création actuelle de la route est déléguée à une nouvelle méthode protégée appelée getRouteForToggleActive()
.
protected function getRouteForToggleActive() { $url = sprintf( '%s/:%s/toggleActive.:sf_format', $this->options['prefix_path'], $this->options['column'] ); $params = array( 'module' => $this->options['module'], 'action' => 'toggleActive', 'sf_format' => 'html' ); $requirements = array('sf_method' => 'put'); $options = array( 'model' => $this->options['model'], 'type' => 'object', 'method' => $this->options['model_methods']['object'] ); return new $this->routeClass( $url, $params, $requirements, $options ); }
La dernière étape restante consiste à configurer la collection de routes dans le fichier routing.yml
. On remarque au passage que la méthode generateRoutes()
cherche après une option intitulée with_is_active
avant d'ajouter la nouvelle route. Ajouter cette logique à la classe donne davantage de contrôle au développeur dans le cas où il souhaiterait utiliser la classe acClientObjectRouteCollection
autre part dans le futur, sans pour autant avoir besoin de la route toggleActive
.
# apps/frontend/config/routing.yml pageAdmin: class: acClientObjectRouteCollection options: model: Page prefix_path: /pages module: pageAdmin with_is_active: true
L'exécution de la tâche app:routes
permet de vérifier que la nouvelle route toggleActive
est présente. La dernière pièce du puzzle manquante est la création de l'action qui s'occupera de cette fonctionnalité. Par ailleurs, si cette collection de routes est amenée à être réutilisée à travers d'autres modules, il suffit simplement de créer un nouveau fichier backendActions.php
dans le répertoire apps/backend/lib/actions
du projet. Le répertoire actions
doit être créé manuellement.
# apps/backend/lib/action/backendActions.class.php class backendActions extends sfActions { public function executeToggleActive(sfWebRequest $request) { $obj = $this->getRoute()->getObject(); $obj->is_active = !$obj->is_active; $obj->save(); $this->redirect($this->getModuleName().'/index'); } }
Enfin, il ne reste plus qu'à changer la classe de base de la classe pageAdminActions
afin que cette dernière hérite de la nouvelle classe backendActions
.
class pageAdminActions extends backendActions { // ... }
Quelles sont les étapes accomplies ? En ajoutant une route à la collection de routes ainsi qu'une action associée dans une classe d'actions de base ; n'importe quel module est désormais capable d'utiliser automatiquement cette fonctionnalité en utilisant une route acClientObjectRouteCollection
et en étendant la classe backendActions
. Par conséquent, une fonctionnalité commune peut être facilement mutualisée et capitalisée à travers plusieurs modules.
Options d'une Collection de Routes
Les collections de routes d'objet contiennent une série d'options qui assurent une personnalisation pointue. En de nombreuses circonstances, un développeur peut ainsi utiliser ces options afin de configurer la collection, sans avoir besoin de créer une nouvelle classe personnalisée de collection de routes. Une liste détaillée des options des collections de routes est disponible dans le Guide de Référence de symfony.
Les Routes d'Actions
Chaque collection de routes d'objet accepte trois options différentes qui déterminent les routes exactes générées dans la collection. Sans pour autant entrer profondément dans le détail, la collection suivante génère les sept routes par défaut, auxquelles s'ajoutent une route de collection d'objets et une route d'objet.
pageAdmin: class: acClientObjectRouteCollection options: # ... actions: [list, new, create, edit, update, delete, show] collection_actions: indexAlt: [get] object_actions: toggle: [put]
Changer la Colonne Discriminante
Par défaut, le framework de routage de symfony utilise la clé primaire d'un modèle lorsqu'il s'agit d'interroger la base de données pour récupérer des objets. Cette information peut bien évidemment être modifiée facilement. Par exemple, le code ci-dessous s'appuiera sur la colonne slug
pour récupérer un objet au lieu de sa clé primaire.
pageAdmin: class: acClientObjectRouteCollection options: # ... column: slug
Modifier les Méthodes du Modèle
La route récupère par défaut tous les objets en relation pour une route de la collection et interroge la base de données sur la colonne (column
) spécifiée lorsqu'il s'agit des routes d'objet. Une fois de plus, ce comportement peut être modifié et surchargé en ajoutant l'option model_methods
à la route. Dans l'exemple ci-dessous, les méthodes fetchAll()
et findForRoute()
devront être définies dans la classe PageTable
. Elles reçoivent toutes les deux un tableau de paramètres de l'objet de requête en guise d'argument.
pageAdmin: class: acClientObjectRouteCollection options: # ... model_methods: list: fetchAll object: findForRoute
Modifier les Paramètres par Défaut
Enfin, il arrive parfois qu'il faille rendre un paramètre spécifique disponible dans l'objet de requête pour chaque route de la collection. Toutes les collections de route bénéficient d'une option supplémentaire default_params
qui permet d'y parvenir facilement.
pageAdmin: class: acClientObjectRouteCollection options: # ... default_params: foo: bar
Conclusion
La principale responsabilité du framework de routage de symfony était à l'origine de faire correspondre URLs et de générer des URLs. Cependant, il a finalement très vite évolué vers un système entièrement personnalisable et capable de s'adapter à la plupart des besoins d'URLs complexes dans un projet.
En prenant le contrôle sur les objets de route, la structure spéciale d'une URL peut ainsi être abstraite en dehors de la logique métier, et conservée entièrement à l'intérieur de la route à laquelle elle appartient. Il en résulte alors davantage de contrôle, de flexibilité et un code beaucoup plus maniable.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.