Aunque ZF3 nos provee de muchos tipos de rutas, en algunas situaciones, necesitaremos escribir nuestro propio tipo de ruta.
Un ejemplo en el que necesitamos un tipo de ruta a la medida es cuando tenemos que definir reglas dinámicas de asociación de URLs. Usualmente se almacenan las configuraciones de rutas en el archivo de configuración de modulo pero en algunos sistemas CMS tendrás documentos guardados en la base de datos. Un sistema como este necesitará del desarrollo de un tipo de ruta a la medida que se pueda conectar a la base de datos y ejecutar la comparación de rutas contra los datos guardados en base de datos. No podemos guardar esta información en archivos de configuración por que los nuevos documentos los guarda el administrador del sistema y no el programador.
Sabemos que cada clase ruta debe implementar la interface Zend\Router\Http\RouteInterface
.
Los métodos de esta interface se presenta en al tabla 5.4:
Nombre del Método | Descripción |
---|---|
factory($options) |
Método estático para la creación de la clase ruta. |
match($request) |
Método que ejecuta la comparación contra los datos de la petición HTTP. |
assemble($params, $options) |
Método para generar la URL a partir de parámetros de ruta. |
getAssembledParams() |
Método para recuperar los parámetros que fueron utilizados en la generación de la URL. |
El método estático factory()
es usado por el router de ZF3 (TreeRouteStack
or SimpleRouteStack
)
para instanciar la clase ruta. El router pasa el arreglo options
como un argumento
al método factory()
.
El método match()
se usa para ejecutar la comparación de la petición HTTP (particularmente
su URL) contra las opciones pasadas a la clase ruta a través de factory()
. El
método match()
debería retornar o una instancia de la clase RouteMatch
en caso
de éxito en la comparación o null
en caso de fallo.
Los parámetros y opciones de la ruta junto con el método assemble()
se usan para
generar la URL. El propósito del método ayudante getAssembledParams()
es regresar
el arreglo de parámetros que fueron usados en la generación de la URL.
Para demostrar la creación de un tipo de ruta a la medida vamos a mejorar nuestra
solución anterior, el sistema de documentación simple que usa un tipo de ruta Regex.
La desventaja del tipo de ruta Regex es que no podemos organizar las páginas estáticas
en jerarquía cuando se crean subcarpetas bajo la carpeta doc (cuando generamos
una URL para cada página la barra de separación de directorios será codificada haciendo
al enlace inútil). Crearemos nuestro propia clase StaticRoute
que permite corregir
este problema.
Además, la clase que crearemos es más poderosa, por que esta no solo reconocerá las URLs que comienzan con "/doc" y terminal con ".html". Adicionalmente reconocerá URLs genéricas como "/help" o "/support/chapter1/introduction".
Lo que queremos alcanzar:
La clase StaticRoute
debe ser insertable en la pila de rutas (SimpleRouteStack
o TreeRouteStack
) y usarse junto con otros tipos de ruta.
La clase ruta debe reconocer URLs genéricas como "/help" o "/introduction".
La clase ruta debe comparar la URL contra la estructura de directorios. Por ejemplo, si la URL es "/chapter1/introduction" entonces la ruta debe revisar si la plantilla de vista correspondiente <base_dir>/chapter1/introduction.phtml existe y es legible y si es así reportar una coincidencia. Si el archivo no existe (o no es legible) regresa un estado fallido.
La clase ruta debe revisar la URL usando una expresión regular que determina si es un nombre de archivo aceptable. Por ejemplo, el nombre de archivo "introduction" es aceptable pero el nombre "*int$roduction" no lo es. Si el nombre del archivo no es aceptable se debe retornar un estado de error.
La clase ruta debe ser capaz de ensamblar la URL con el nombre de ruta y sus parámetros.
Para comenzar creamos la subcarpeta Route bajo el directorio fuente del módulo y colocamos el archivo StaticRoute.php dentro de él (figura 5.9).
Dentro del archivo pegamos este pedazo de código:
<?php
namespace Application\Route;
use Traversable;
use \Zend\Router\Exception;
use \Zend\Stdlib\ArrayUtils;
use \Zend\Stdlib\RequestInterface as Request;
use \Zend\Router\Http\RouteInterface;
use \Zend\Router\Http\RouteMatch;
// Custom route that serves "static" web pages.
class StaticRoute implements RouteInterface
{
// Create a new route with given options.
public static function factory($options = [])
{
}
// Match a given request.
public function match(Request $request, $pathOffset = null)
{
}
// Assembles a URL by route params.
public function assemble(array $params = [], array $options = [])
{
}
// Get a list of parameters used while assembling.
public function getAssembledParams()
{
}
}
Del código de arriba debemos notar que se coloco a la clase StaticRoute
dentro
del namespace Application\Route
(línea 2).
En las líneas 4-9 definimos algunos alias de nombre de clase para hacer al nombre de la clase mas corto.
En las líneas 12-33 definimos el esqueleto para la clase StaticRoute
. La clase
StaticRoute
implementa a la interface RouteInterface
y define todos los métodos
especificados en la interface: factory()
, match()
, assemble()
y getAssembledParams()
.
Luego vamos a agregar varias propiedades protegidas y el método constructor de la
clase StaticRoute
como se muestra abajo:
<?php
//...
class StaticRoute implements RouteInterface
{
// Base view directory.
protected $dirName;
// Path prefix for the view templates.
protected $templatePrefix;
// File name pattern.
protected $fileNamePattern = '/[a-zA-Z0-9_\-]+/';
// Defaults.
protected $defaults;
// List of assembled parameters.
protected $assembledParams = [];
// Constructor.
public function __construct($dirName, $templatePrefix,
$fileNamePattern, array $defaults = [])
{
$this->dirName = $dirName;
$this->templatePrefix = $templatePrefix;
$this->fileNamePattern = $fileNamePattern;
$this->defaults = $defaults;
}
// ...
}
Arriba en la línea 7 definimos la propiedad $dirName
que guardará el nombre del
directorio base donde las plantillas "estáticas" de vista se almacenan. En la línea
10 definimos la variable de clase $templatePrefix
que guarda el prefijo prefijado
de todos las plantillas de vista. La línea 13 contiene la variable $fileNamePattern
que se usará para revisar el nombre de archivo.
En las líneas 22-29 definimos el método constructor que se llama cuando se crear una nueva instancia de la clase con lo que se inicializan las propiedades protegidas.
Luego, vamos a implementar el método factory()
de nuestra clase a la medida
StaticRoute
. El router llamará al método factory()
para instanciar la clase route:
<?php
//...
class StaticRoute implements RouteInterface
{
//...
// Create a new route with given options.
public static function factory($options = [])
{
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
} elseif (!is_array($options)) {
throw new Exception\InvalidArgumentException(__METHOD__ .
' expects an array or Traversable set of options');
}
if (!isset($options['dir_name'])) {
throw new Exception\InvalidArgumentException(
'Missing "dir_name" in options array');
}
if (!isset($options['template_prefix'])) {
throw new Exception\InvalidArgumentException(
'Missing "template_prefix" in options array');
}
if (!isset($options['filename_pattern'])) {
throw new Exception\InvalidArgumentException(
'Missing "filename_pattern" in options array');
}
if (!isset($options['defaults'])) {
$options['defaults'] = [];
}
return new static(
$options['dir_name'],
$options['template_prefix'],
$options['filename_pattern'],
$options['defaults']);
}
}
En el código de arriba vemos que el método factory()
toma el arreglo options
como argumento (línea 9). El arreglo options
puede contener las opciones de
configuración de la clase route. La clase StaticRoute
aceptará las siguientes
opciones:
dir_name
- directorio base donde se almacenan todas plantillas de vista "estáticas".template_prefix
- el prefijo que se prefija al nombre de todas las plantillas.filename_pattern
- la expresión regular que revisa el nombre de los archivos.defaults
- los parámetros regresados por el router por defecto.Una vez que se analizan las opciones llamamos al método constructor de la clase
en las líneas 37-41 para inicializar y regresar el objeto StaticRoute
.
El siguiente método que agregamos a la clase de ruta StaticRoute
es el método
match()
:
<?php
//...
class StaticRoute implements RouteInterface
{
//...
// Match a given request.
public function match(Request $request, $pathOffset=null)
{
// Ensure this route type is used in an HTTP request
if (!method_exists($request, 'getUri')) {
return null;
}
// Get the URL and its path part.
$uri = $request->getUri();
$path = $uri->getPath();
if($pathOffset!=null)
$path = substr($path, $pathOffset);
// Get the array of path segments.
$segments = explode('/', $path);
// Check each segment against allowed file name template.
foreach ($segments as $segment) {
if(strlen($segment)==0)
continue;
if(!preg_match($this->fileNamePattern, $segment))
return null;
}
// Check if such a .phtml file exists on disk
$fileName = $this->dirName . '/'.
$this->templatePrefix.$path.'.phtml';
if(!is_file($fileName) || !is_readable($fileName)) {
return null;
}
$matchedLength = strlen($path);
// Prepare the RouteMatch object.
return new RouteMatch(array_merge(
$this->defaults,
['page'=>$this->templatePrefix.$path]
),
$matchedLength);
}
}
En el código de arriba vemos que el método match()
toma dos argumentos: el objeto
de la petición HTTP (una instancia de la clase Zend\Stdlib\Request
) y la ruta offset
URL. El objeto de la petición se usa para conseguir la URL de la petición (línea 17).
El parámetro de ruta offset es un entero positivo que apunta a la porción de la URL
contra la que se compara la ruta (línea 21).
En la línea 24 extraemos los segmentos de la URL. Luego revisamos si cada segmento
es un nombre de archivo o directorio valido (líneas 27-32). Si el segmento no
es un nombre de archivo valido regresamos null
como un estado fallido.
En la línea 35 calculamos la ruta de la plantilla de vista y en las líneas 37-39 revisamos si el archivo realmente existe y se puede leer. De esta manera comparamos la URL contra la estructura del directorio.
En las líneas 44-48 preparamos y regresamos el objeto RouteMatch
con los parámetros
por defecto más el parámetro "página" que contiene el nombre de la plantilla de vista
que se va a mostrar.
Para completar la implementación de nuestra clase StaticRoute
agregamos los métodos
assemble()
y getAssembledParams()
que se usarán para generar las URLs a partir
de los parámetros de una ruta. El código de estos métodos se muestra abajo:
<?php
//...
class StaticRoute implements RouteInterface
{
//...
// Assembles a URL by route params
public function assemble(array $params = [],
array $options = [])
{
$mergedParams = array_merge($this->defaults, $params);
$this->assembledParams = [];
if(!isset($params['page'])) {
throw new Exception\InvalidArgumentException(__METHOD__ .
' expects the "page" parameter');
}
$segments = explode('/', $params['page']);
$url = '';
foreach($segments as $segment) {
if(strlen($segment)==0)
continue;
$url .= '/' . rawurlencode($segment);
}
$this->assembledParams[] = 'page';
return $url;
}
// Get a list of parameters used while assembling.
public function getAssembledParams()
{
return $this->assembledParams;
}
}
En el código de arriba definimos el método assemble()
que toma dos argumentos:
El arreglo parameters
y el arreglo options
(línea 9). El método construye la
URL codificando los segmentos con la codificación URL para luego concatenarlos.
El método getAssembledParams()
regresa exactamente los nombres de los parámetros
usados para la generación de la URL (línea 36).
Hemos terminado con la clase de ruta StaticRoute
. Para usar nuestro tipo de ruta,
hecha a la medida, agregamos la siguiente configuración al archivo de configuración
module.config.php:
Now we've finished the StaticRoute
route class. To use our custom route type,
we add the following configuration to the module.config.php configuration file:
'static' => [
'type' => StaticRoute::class,
'options' => [
'dir_name' => __DIR__ . '/../view',
'template_prefix' => 'application/index/static',
'filename_pattern' => '/[a-z0-9_\-]+/',
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => 'static',
],
],
],
En la línea 1 de la configuración de arriba definimos una regla de routing llamada
"static". El parámetro type
define el nombre completo de la clase StaticRoute
(línea 2). En el arreglo options
definimos el directorio base donde las páginas
"estáticas" estarán almacenadas (línea 4), el prefijo de las plantillas (linea 5),
el patrón para el nombre del archivo (línea 6) y el arreglo defaults
que contiene
el nombre del controlador y la acción que servirá todas las páginas estáticas.
No olvidemos insertar la siguiente línea al comienzo de la clase en el archivo
module.config.php
:
use Application\Route\StaticRoute;
El último paso es crear el método de acción en la clase IndexController
:
public function staticAction()
{
// Get path to view template from route params
$pageTemplate = $this->params()->fromRoute('page', null);
if($pageTemplate==null) {
$this->getResponse()->setStatusCode(404);
return;
}
// Render the page
$viewModel = new ViewModel([
'page'=>$pageTemplate
]);
$viewModel->setTemplate($pageTemplate);
return $viewModel;
}
La acción de arriba es casi idéntica a la acción usada para la ruta Regex. En
la línea 4 recuperamos el parámetro page
de la ruta y lo guardamos en la variable
$pageTemplate
. En la línea 11 creamos la variable contenedor ViewModel
y en la
línea 14 colocamos explícitamente el nombre de la plantilla de vista que se va a mostrar.
Para ver el sistema en acción vamos a agregar un par de páginas de vista "estáticas":
la página de ayuda (help.phtml
) y la página de introducción (intro.phtml
).
Creamos la subcarpeta static dentro del directorio view/application/index del
módulo Application
y colocamos plantilla de vista help.phtml allí:
<h1>Help</h1>
<p>
See the help <a href="<?= $this->url('static',
['page'=>'/chapter1/intro']); ?>">introduction</a> here.
</p>
Luego creamos la subcarpeta chapter1 dentro del directorio static y colocamos el archivo chapter1/intro.phtml allí:
<h1>Introduction</h1>
<p>
Write the help introduction here.
</p>
Finalmente deberíamos tener la siguiente estructura de directorios (figura 5.10):
Finalmente, escribimos la siguiente URL en el navegador web: http://localhost/help. La página de ayuda debería aparecer (ver figura 5.11). Si escribimos la URL http://localhost/chapter1/intro en nuestro navegador deberíamos ver la página Introduction (figura 5.12).
Podemos crear páginas estáticas agregando archivos phtml al directorio static y ellos automáticamente estarán disponibles para los usuarios del sitio.
Si nos encontramos atascados podemos encontrar este ejemplo completo y trabajando dentro de la aplicación Hello World.