A free and open-source book on ZF3 for beginners

Translation into this language is not yet finished. You can help this project by translating the chapters and contributing your changes.

5.11. Escribir nuestra Propia Ruta

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.

5.11.1. RouteInterface

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:

Tabla 5.4. Métodos RouteInterface
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.

5.11.2. Clase Rute a la Medida

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:

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

Figure 5.9. Archivo StaticRoute.php Figure 5.9. Archivo StaticRoute.php

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:

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):

Figure 5.10. Static pages Figure 5.10. Static pages

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

Figure 5.11. Help page Figure 5.11. Help page

Figure 5.12. Introduction page Figure 5.12. Introduction page

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.


Top