por Ryan Weaver
El framework de enrutamiento es básicamente un mapa que enlaza cada URL con un lugar específico dentro de un proyecto Symfony y viceversa. Gracias al enrutamiento se pueden crear URL bonitas que además son completamente independientes de la lógica de la aplicación. Con los avances incorporados en las versiones más recientes de Symfony, el framework de enrutamiento es todavía más completo.
En este capítulo se muestra cómo crear una aplicación web sencilla en la que
cada usuario utiliza un subdominio diferente (cliente1.midominio.com
y
cliente2.midominio.com
). Esto se puede conseguir fácilmente extendiendo el
framework de enrutamiento.
note
Este capítulo requiere utilizar Doctrine como ORM del proyecto.
Preparación del proyecto: un CMS para muchos clientes
En este proyecto, una empresa imaginaria llamada Sympal Builder quiere crear
un CMS para que sus clientes puedan construir sitios web como subdominios de
sympalbuilder.com
. En concreto, el cliente XXX puede ver su sitio web en
xxx.sympalbuilder.com
y hacer uso del área de administración en
xxx.sympalbuilder.com/backend.php
.
note
El nombre Sympal
se ha tomado prestado del proyecto Sympal
creado por Jonathan Wage, que es un framework de gestores de contenidos (CMF)
desarrollado con Symfony.
Los dos requerimientos básicos del proyecto son:
Los usuarios pueden crear páginas y especificar el título, contenido y URL de esas páginas.
Toda la aplicación debe construirse dentro de un único proyecto de Symfony que gestione el frontend y backend de todos los sitios de los clientes y que obtenga los datos adecuados en función del subdominio utilizado por cada cliente.
note
Para crear esta aplicación, el servidor debe configurarse para redirigir todos
los subdominios *.sympalbuilder.com
al mismo directorio raíz, que es el
directorio web del proyecto Symfony.
El esquema y los datos
La base de datos del proyecto está formada por clientes (objeto Client
) y
páginas (objeto Page
). Cada cliente representa un sitio web accesible mediante
un subdominio y puede contener varias páginas.
# 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
Aunque los índices de cada tabla no son obligatorios, es mejor añadirlos porque la aplicación va a realizar muchas consultas que utilizan estas columnas.
Para poder probar el funcionamiento del proyecto, añade los siguientes datos de
prueba en el archivo 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
Los datos de prueba crean dos sitios web, cada uno de ellos con una página. La
URL completa de cada página está formada por el valor de la columa subdomain
de la tabla Client
y por el valor de la columna slug
del objeto Page
.
http://pete.sympalbuilder.com/location http://citypub.sympalbuilder.com/menu
El enrutamiento
Todas las páginas de los sitios web creados en Sympal Builder se corresponden
de forma directa con un objeto de tipo Page
del modelo, que define el título
y el contenido que se muestran. Para asociar cada URL con su correspondiente
objeto Page
, se crea una nueva ruta de objetos de tipo sfDoctrineRoute
y
que hace uso del campo slug
. El siguiente código busca automáticamente en la
base de datos un objeto de tipo Page
cuyo campo slug
coincida con el que
incluye la URL:
# apps/frontend/config/routing.yml page_show: url: /:slug class: sfDoctrineRoute options: model: Page type: object params: module: page action: show
La ruta anterior asocia correctamente la página http://pete.sympalbuilder.com/location
con su objeto Page
. Desafortunadamente, la ruta anterior también funciona con
la URL http://pete.sympalbuilder.com/menu
, por lo que el menú del restaurante
se mostraría en el sitio web de Peter. Por el momento, el enrutamiento no es
consciente de la importancia de los subdominios de los clientes.
Para que la aplicación funcione correctamente, el enrutamiento debe ser más
avanzado. El objeto Page
se debe buscar tanto por el campo slug
como por el
campo client_id
. Este último campo se puede determinar comparando el host
(por ejemplo pete.sympalbuilder.com
) con el valor de la columna subdomain
del modelo Client
. Para ello, se va a mejorar el framework de enrutamiento
creando una clase propia de enrutamiento. No obstante, antes de crear esta clase
será necesario repasar cómo funciona el sistema de enrutamiento.
Cómo funciona el sistema de enrutamiento
Las rutas de Symfony son objetos de tipo sfRoute
que tienen dos importantes
tareas:
Generar URL: si por ejemplo se pasa al método
page_show
un parámetro llamadoslug
, debería ser capaz de generar una URL real (por ejemplo,/location
).Procesar las URL entrantes: a partir de la URL de una petición, cada ruta debe ser capaz de determinar si la URL cumple los requisitos de la ruta.
La información de cada ruta individual normalmente se configura en el archivo
app/mi_aplicacion/config/routing.yml
que se encuentra en el directorio de configuración
de cada aplicación. Si una ruta es "un objeto de tipo sfRoute
", ¿cómo se transforma
la configuración YAML en objetos sfRoute
?
Gestor de configuración de la cache del enrutamiento
Aunque las rutas se definen en un archivo YAML, cada entrada de ese archivo se
transforma en un objeto durante la petición mediante un tipo especial de clase
llamada gestor de configuración de la cache. El resultado es código PHP que representa a todas
y cada una de las rutas de la aplicación. Aunque los detalles de funcionamiento
de este proceso están fuera del alcance de este capítulo, se muestra a continuación
parte de la versión compilada final de la ruta page_show
. El archivo compilado
se encuentra en cache/mi_aplicacion/mi_entorno/config/config_routing.yml.php
y depende de la aplicación y del entorno. A continuación se muestra un pequeño
extracto de la ruta page_show
completa:
new sfDoctrineRoute('/:slug', array ( 'module' => 'page', 'action' => 'show', ), array ( 'slug' => '[^/\\.]+', ), array ( 'model' => 'Page', 'type' => 'object', ));
tip
El nombre de la clase de cada ruta se define en la clave class
del archivo
routing.yml
. Si no se especifica una clave class
, por defecto se considera
que es una clase de tipo sfRoute
. Otra clase de ruta común es sfRequestRoute
que permite al programador crear rutas RESTful. El libro
The symfony Reference Book
incluye la lista completa de clases de ruta y todas sus opciones.
Asociando una petición con una ruta específica
Una de las tareas principales del framework de enrutamiento consiste en asociar
cada URL entrante con su objeto de ruta correcto. La clase sfPatternRouting
es el núcleo central del enrutamiento y se encarga de realizar este proceso. A
pesar de su importancia, los programadores no interactúan casi nunca de forma
directa con sfPatternRouting
.
Para asociar la ruta correcta, sfPatternRouting
itera por cada clase sfRoute
preguntando si el patrón de la ruta coincide con la URL entrante. Internamente
sfPatternRouting
ejecuta el método sfRoute::matchesUrl()
sobre cada
objeto de ruta. Este método simplemente devuelve false
si el patrón de la ruta
no coincide con la URL entrante.
Cuando el patrón de la ruta coincide, el método sfRoute::matchesUrl()
hace
mucho más que devolver true
. En este caso, la ruta devuelve un array de
parámetros que se incluyen en el objeto de la petición. La URL
http://pete.sympalbuilder.com/location
por ejemplo está asociada con la ruta
page_show
, cuyo método matchesUrl()
devolvería el siguiente array:
array('slug' => 'location')
Esta información se incluye después en el objeto de la petición, por lo que es
posible acceder a las variables de la ruta (por ejemplo slug
) desde las acciones
y otros lugares del proyecto.
$this->slug = $request->getParameter('slug');
Como puede que ya hayas adivinado, redefinir el método sfRoute::matchesUrl()
es la mejor forma de personalizar las rutas para que hagan cualquier cosa.
Creando una clase de ruta personalizada
A continuación se crea una nueva clase de ruta para extender la ruta page_show
de forma que tenga en cuenta el subdominio de los objetos Client
. Para ello,
crea un archivo llamado acClientObjectRoute.class.php
en el directorio
lib/routing
del proyecto (debes crear este directorio manualmente):
// 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; } }
El único paso que falta es indicar a la ruta page_show
que utilice esta nueva
clase de ruta. Actualiza el valor de la clave class
de la ruta en el archivo
routing.yml
:
# apps/fo/config/routing.yml page_show: url: /:slug class: acClientObjectRoute options: model: Page type: object params: module: page action: show
Aunque el uso de la clase acClientObjectRoute
todavía no añade ninguna
funcionalidad, la aplicación ya está preparada para funcionar como se espera.
El método matchesUrl()
se encarga principalmente de dos tareas.
Añadiendo la lógica en la ruta personalizada
Para que la ruta propia incluya la funcionalidad requerida, reemplaza los
contenidos del archivo acClientObjectRoute.class.php
por lo siguiente.
class acClientObjectRoute extends sfDoctrineRoute { protected $baseHost = '.sympalbuilder.com'; public function matchesUrl($url, $context = array()) { if (false === $parameters = parent::matchesUrl($url, $context)) { return false; } // devuelve false si no se encuentra el valor de "baseHost" 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); } }
La llamada inicial al método parent::matchesUrl()
es importante porque ejecuta
el proceso normal de comprobación de las rutas. En este ejemplo, como la URL
/location
cumple con el patrón de la ruta page_show
, el método
parent::matchesUrl()
devolvería un array que contiene el parámetro slug
.
array('slug' => 'location')
En otras palabras, el trabajo duro del enrutamiento se realiza de forma
automática, por lo que el resto del método se puede dedicar a obtener el objeto
Client
correcto para ese subdominio.
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); }
Realizando una sustitución en la cadena de texto se puede obtener la parte del
subdominio del host y después realizar una consulta en la base de datos para
determinar si algún objeto Client
tiene este subdominio. Si no existen objetos
Client
con ese subdominio, se devuelve el valor false
para indicar que la
petición entrante no cumple con el patrón de esta ruta. Si por el contrario
existe un objeto Client
con ese subdominio, se añade un nuevo parámetro
llamado client_id
en el array que se devuelve.
tip
El array $context
que se pasa a matchesUrl()
incluye mucha información
útil sobre la petición actual, incluyendo el host
, un valor booleano que
indica si la petición es segura (is_secure
), la URI de la petición (request_uri
),
el método de HTTP (method
) y mucho más.
¿Qué es lo que se ha conseguido con esta ruta personalizada? Básicamente la
clase acClientObjectRoute
ahora realiza lo siguiente:
La
$url
entrante sólo cumplirá el patrón de la ruta si elhost
contiene un subdominio que pertenezca a alguno de los objetosClient
.Si se cumple el patrón de la ruta, se devuelve un parámetro adicional llamado
client_id
, obtenido del objetoClient
y que se añade al resto de parámetros de la petición.
Haciendo uso de la ruta propia
Una vez que acClientObjectRoute
devuelve el parámetro client_id
correcto,
la acción puede obtenerlo a través del objeto de la petición. La acción
page/show
podría utilizar por ejemplo el parámetro client_id
para encontrar
el objeto Page
correcto:
public function executeShow(sfWebRequest $request) { $this->page = Doctrine_Core::getTable('Page')->findOneBySlugAndClientId( $request->getParameter('slug'), $request->getParameter('client_id') ); $this->forward404Unless($this->page); }
note
El método findOneBySlugAndClientId()
es un nuevo tipo de
buscador mágico
de Doctrine 1.2 que busca objetos en función de varios campos.
El framework de enrutamiento permite aplicar una solución todavía más elegante.
En primer lugar, añade el siguiente método a la clase acClientObjectRoute
:
protected function getRealVariables() { return array_merge(array('client_id'), parent::getRealVariables()); }
Gracias a este último método, la acción puede obtener el objeto Page
correcto
directamente desde la ruta. Por tanto, la acción page/show
se puede reducir
a una única línea de código.
public function executeShow(sfWebRequest $request) { $this->page = $this->getRoute()->getObject(); }
Sin necesidad de añadir más código, la instrucción anterior busca un objeto de
tipo Page
en función de las columnas slug
y client_id
. Además, al igual
que el resto de rutas de objetos, la acción redirige de forma automática a la
página del error 404 si no se encuentra ningún objeto.
¿Cómo funciona? Las rutas de objetos, como sfDoctrineRoute
, utilizada por la
clase acClientObjectRoute
, busca automáticamente el objeto relacionado en
función de las variables de la clave url
de la ruta. La ruta page_show
por
ejemplo contiene la variable :slug
en su url
, por lo que busca el objeto
Page
mediante el valor de la columna slug
.
No obstante, en esta aplicación la ruta page_show
también debe buscar los
objetos Page
en función de la columna client_id
. Para ello, se ha redefinido
el método sfObjectRoute::getRealVariables()
, que se invoca internamente
para obtener las columnas con las que se realiza la consulta. Añadiendo el campo
client_id
en este array, acClientObjectRoute
buscará los objetos haciendo
uso de las columnas slug
y client_id
.
note
Las rutas de objetos ignoran automáticamente cualquier variable que no se
corresponda a una columna real. Si por ejemplo la URL contiene una variable
llamada :page
pero la tabla no contiene una columna page
, esta variable se
ignora.
A estas alturas, ya hemos conseguido que la clase de ruta propia realice todo lo necesario. En las próximas secciones se reutiliza esta nueva ruta para crear un área de administración específico para cada cliente.
Generando la ruta correcta
Aún existe un pequeño problema sobre cómo se genera la ruta. Imagina que se crea un enlace a una página utilizando el siguiente código:
<?php echo link_to('Locations', 'page_show', $page) ?>
URL generada: /location?client_id=1
Como puedes observar, el valor de client_id
se ha añadido automáticamente al
final de la URL. Esto sucede porque la ruta trata de utilizar todas sus variables
para generar la URL. Como la ruta dispone de un parámetro llamado slug
y de
otro parámetro llamado client_id
, hace uso de los dos al generar la ruta.
Para solucionarlo, añade el siguiente método a la clase acClientObjectRoute
:
protected function doConvertObjectToArray($object) { $parameters = parent::doConvertObjectToArray($object); unset($parameters['client_id']); return $parameters; }
Cuando se genera una ruta de objetos, se obtiene toda la información necesaria
invocando el método doConvertObjectToArray()
. Por defecto se devuelve client_id
en el array $parameters
. Al eliminar esa variable, se evita que se incluya
en la URL generada. Recuerda que esto es posible porque la información del
objeto Client
se guarda en el propio subdominio.
tip
Puedes redefinir completamente el proceso de doConvertObjectToArray()
y
gestionarlo tu mismo añadiendo un método llamado toParams()
en la clase del
modelo. Este método debe devolver un array con los parámetros que quieres que
se utilicen al generar la ruta.
Colecciones de rutas
Para finalizar la aplicación Sympal Builder, es preciso crear un área de
administración individual para que cada cliente (Client
) pueda gestionar sus
páginas (Pages
). Para ello, se necesitan varias acciones que permitan listar,
crear, actualizar y borrar los objetos de tipo Page
. Como este tipo de acciones
son muy comunes, Symfony puede generar automáticamente el módulo completo.
Ejecuta la siguiente tarea en la línea de comandos para generar un módulo
llamado pageAdmin
dentro de la aplicación llamada backend
:
$ php symfony doctrine:generate-module backend pageAdmin Page --with-doctrine-route --with-show
La tarea anterior genera un módulo con un archivo de acciones y todas las
plantillas necesarias para realizar cualquier modificación sobre los objetos
Page
. Aunque se pueden realizar muchas modificaciones sobre estas acciones y
plantillas generadas, es algo que está fuera del alcance de este capítulo.
Aunque la tarea anterior genera un módulo completo, todavía es necesario crear
una ruta para cada acción. La opción --with-doctrine-route
que se ha pasado
a la tarea hace que todas las acciones generadas funcionen con una ruta de
objeto. De esta forma se reduce el código de cada acción. La siguiente acción
edit
contiene por ejemplo una única línea:
public function executeEdit(sfWebRequest $request) { $this->form = new PageForm($this->getRoute()->getObject()); }
Todas las rutas necesarias son index
, new
, create
, edit
, update
,
y delete
. Normalmente crear estas rutas de tipo
RESTful
requeriría añadir lo siguiente en el archivo 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]
Para ver estas rutas, ejecuta la tarea app:routes
, que muestra un resumen de
cada ruta de la aplicación indicada:
$ 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
Sustituyendo las rutas por una colección de rutas
Afortunadamente Symfony permite añadir todas las rutas relacionadas con el CRUD
de forma mucho más concisa. Reemplaza el contenido del archivo routing.yml
por la siguiente ruta.
pageAdmin: class: sfDoctrineRouteCollection options: model: Page prefix_path: /pages module: pageAdmin
Ejecuta de nuevo la tarea app:routes
para visualizar todas las rutas. Como
puedes ver, todavía se muestran las siete rutas anteriores.
$ 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
Las colecciones de rutas son un tipo especial de objeto que internamente
representan más de una ruta. La ruta sfDoctrineRouteCollection
por ejemplo
genera automáticamente las siete rutas habitualmente necesarias para el CRUD.
En realidad, la ruta sfDoctrineRouteCollection
crea internamente las mismas
siete rutas que se incluyeron antes en el archivo routing.yml
. Las colecciones
de rutas básicamente existen como atajo para crear grupos comunes de rutas.
Creando una colección de rutas propia
En estos momentos, cada cliente (Client
) puede modificar sus objetos página
(Page
) mediante un CRUD accedido a través de la URL /pages
. Desafortunadamente,
cada cliente puede ver y modificar todos los objetos Page
, incluso los que
no le pertenecen. La URL http://pete.sympalbuilder.com/backend.php/pages
por
ejemplo mostrará una lista de todas las páginas creadas mediante el archivo
de datos - la página location
de la tienda de animales de Pete y la página
menu
del City Pub.
Para solucionar este problema, se va a reutilizar la ruta propia acClientObjectRoute
que se creo para el frontend. La clase sfDoctrineRouteCollection
genera un
grupo de objetos sfDoctrineRoute
. No obstante, en esta aplicación es necesario
generar un grupo de objetos acClientObjectRoute
.
Para ello, se va a utilizar una colección de rutas propia. Crea un nuevo archivo
llamado acClientObjectRouteCollection.class.php
dentro del directorio
lib/routing
. El contenido del archivo es realmente sencillo:
// lib/routing/acClientObjectRouteCollection.class.php class acClientObjectRouteCollection extends sfObjectRouteCollection { protected $routeClass = 'acClientObjectRoute'; }
La propiedad $routeClass
define la clase que se utiliza al crear cada una de
las rutas de la colección. Por tanto, ahora cada ruta individual será de tipo
acClientObjectRoute
. Si se accede ahora a la URL
http://pete.sympalbuilder.com/backend.php/pages
solamente se muestra una página:
la página location
de la tienda de animales de Pete. Gracias a la clase de
ruta propia, la acción index
sólo devuelve los objetos Page
relacionados
con el Client
correcto en función del subdominio de la petición. Como se
acaba de demostrar, es posible crear con unas pocas líneas de código, un módulo
completo del backend que pueden utilizar varios clientes diferentes.
Creando nuevas páginas
Actualmente las páginas de creación y modificación de objetos Page
muestran
una lista desplegable con todos los Client
. En lugar de permitir que los
usuarios puedan elegir el Client
, que además podría comprometer la seguridad,
se va a fijar el Client
automáticamente en función del subdominio de la petición.
En primer lugar, actualiza el objeto PageForm
en lib/form/PageForm.class.php
.
public function configure() { $this->useFields(array( 'title', 'content', )); }
La lista desplegable ya no se muestra en los formularios de tipo Page
. El
problema es que al quitar la lista de clientes, cuando se crean objetos Page
ya no se incluye el valor client_id
. La solución consiste en añadir a mano
el objeto Client
relacionado en las acciones new
y create
.
public function executeNew(sfWebRequest $request) { $page = new Page(); $page->Client = $this->getRoute()->getClient(); $this->form = new PageForm($page); }
El código anterior utiliza un método llamado getClient()
que todavía no existe
en la clase acClientObjectRoute
. A continuación se muestran las modificaciones
necesarias para añadirlo:
// 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; } }
Para hacer que el objeto Client
está disponible a través de la ruta, se añade
una propiedad $client
en la clase y se establece su valor en el método
matchesUrl()
. Ahora los nuevos objetos Page
ya incluirán correctamente el
valor de la columna client_id
en función del subdominio de la petición.
Personalizando una colección de rutas de objeto
Haciendo uso del framework de enrutamiento, hemos solucionado fácilmente los retos planteados al crear una aplicación como Sympal Builder. A medida que la aplicación crezca, podremos reutilizar las rutas propias en otros módulos del área de administración (para que los clientes puedan por ejemplo gestionar sus galerías de fotos).
Otra razón para crear una colección de rutas propia es la posibilidad de añadir
rutas adicionales usadas habitualmente. Imagina que por ejemplo un proyecto
utiliza muchos modelos, cada uno de los cuales dispone de una columna is_active
.
El área de administración debe incluir una forma sencilla de activar o desactivar
el valor is_active
de un objeto. Para ello, en primer lugar modifica la clase
acClientObjectRouteCollection
para incluir una nueva ruta a la colección:
// 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(); } }
El método sfObjectRouteCollection::generateRoutes()
se invoca al instanciar
el objeto de la colección y se encarga de crear todas las rutas necesarias y de
incluirlas en la propiedad $routes
de la clase. En este caso, derivamos la
creación de la ruta a un nuevo método protegido llamado 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 ); }
El único paso que falta es configurar la colección de rutas en el archivo
routing.yml
. Como has podido observar, generateRoutes()
busca una opción
llamada with_is_active
antes de añadir la nueva ruta. Incluir esta comprobación
permite un mayor control en caso de que se quiera reutilizar la colección
acClientObjectRouteCollection
más adelante en algún lugar que no necesite
la ruta toggleActive
:
# apps/frontend/config/routing.yml pageAdmin: class: acClientObjectRouteCollection options: model: Page prefix_path: /pages module: pageAdmin with_is_active: true
Ejecuta la tarea app:routes
y verifica que existe una nueva ruta llamada
toggleActive
. Lo único que falta es crear la acción encargada de realizar
todo el trabajo. Como es posible que se reutilice en varios módulos esta colección
de rutas y su acción asociada, crea un nuevo archivo backendActions.class.php
en el directorio apps/backend/lib/action
(debes crear a mano este directorio):
# 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'); } }
Por último, modifica la clase base de pageAdminActions
para que herede de
esta nueva clase backendActions
.
class pageAdminActions extends backendActions { // ... }
¿Qué es lo que hemos conseguido? Añadir una ruta a la colección de rutas y una
acción asociada permite que cualquier módulo pueda incluir automáticamente esta
funcionalidad simplemente utilizando la colección acClientObjectRouteCollection
y extendiendo la clase backendActions
. De esta forma, es posible reutilizar
las funcionalidades comunes entre varios módulos diferentes.
Opciones de las colecciones de rutas
Las colecciones de rutas de objetos incluyen varias opciones para personalizar completamente su funcionamiento. En muchos casos estas opciones son suficientes para configurar la colección sin necesidad de crear una nueva colección propia. El libro The symfony Reference Book contiene la lista completa de todas las opciones de las colecciones de rutas.
Rutas de la acción
Cada colección de rutas admite tres opciones diferentes que determinan exactamente las rutas que se generan en la colección. Sin entrar en muchos detalles, la siguiente colección generaría las siete rutas por defecto junto con una colección de rutas adicional y una ruta de objeto:
pageAdmin: class: acClientObjectRouteCollection options: # ... actions: [list, new, create, edit, update, delete, show] collection_actions: indexAlt: [get] object_actions: toggle: [put]
Columna
Por defecto todas las URL generadas emplean la clave primaria del modelo, que
también se utiliza para buscar los objetos. Obviamente este comportamiento se
puede modificar con facilidad. El siguiente ejemplo utiliza la columna slug
en vez de la clave primaria:
pageAdmin: class: acClientObjectRouteCollection options: # ... column: slug
Métodos del modelo
Por defecto la ruta obtiene todos los objetos relacionados con la colección
y utiliza la columna especificada en column
para las rutas de los objetos.
Si necesitas redefinir ese comportamiento, añade la opción model_methods
en
la ruta. En este ejemplo se deben añadir los métodos fetchAll()
y findForRoute()
a la clase PageTable
. A los dos métodos se les pasa como argumento un array
con los parámetros de la petición:
pageAdmin: class: acClientObjectRouteCollection options: # ... model_methods: list: fetchAll object: findForRoute
Parámetros por defecto
Por último, imagina que todas las rutas de la colección necesitan un determinado
parámetro en la petición. Esto se puede conseguir fácilmente con la opción
default_params
:
pageAdmin: class: acClientObjectRouteCollection options: # ... default_params: foo: bar
Conclusión
Las tareas habituales del framework de enrutamiento - generar URL y comprobar que cumplen el patrón de las rutas - ha evolucionado hacia un sistema completamente configurable capaz de encargarse de los requerimientos más complejos para las URL de un proyecto. Además, la estructura de las URL se abstrae de la lógica de negocio de la aplicación y se traslada al enrutamiento, que es su lugar natural y donde se controlan todos los objetos de las rutas. El resultado final es un mayor control, más flexibilidad y código más manejable.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.