Más con Symfony

2.3. 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.

2.3.1. 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.

Nota 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 el host contiene un subdominio que pertenezca a alguno de los objetos Client.
  • Si se cumple el patrón de la ruta, se devuelve un parámetro adicional llamado client_id, obtenido del objeto Client y que se añade al resto de parámetros de la petición.

2.3.2. 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);
}

Nota 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.

Nota 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.

2.3.3. 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.

Nota 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.