A free and open-source book on ZF3 for beginners


5.11. Writing Own Route Type

Although ZF3 provides you with many route types, in some situations, you will need to write your own route type.

One example of the need for such a custom route type is when you have to define the URL mapping rules dynamically. Usually, you store the routing configuration in module's config file, but in some CMS systems you will have documents stored in the database. For such a system, you would need to develop a custom route type which would connect to the database and perform route matching against the data stored in the database. You cannot store this information in config file, because new documents are created by system administrators, not programmers.

5.11.1. RouteInterface

We know that every route class must implement the Zend\Router\Http\RouteInterface interface. The methods of this interface are presented in table 5.4:

Table 5.4. RouteInterface methods
Method Name Description
factory($options) Static method for creation of the route class.
match($request) Method which performs match against the HTTP request data.
assemble($params, $options) Method for generating URL by route parameters.
getAssembledParams() Method for retrieving parameters that were utilized for URL generation.

The static factory() method is used by the ZF3 router (TreeRouteStack or SimpleRouteStack) for instantiating the route class. The router passes the options array an argument for the factory() method.

The match() method is used to perform the matching of the HTTP request (or, particularly its URL) against the options data passed to the route class through the factory(). The match() method should return either an instance of the RouteMatch class on successful match, or null on failure.

The assemble() method is used for generating URL string by route parameters and options. The getAssembledParams() helper method's purpose is to return the array of parameters which were used on URL generation.

5.11.2. Custom Route Class

To demonstrate the creation of a custom route type, let's improve our previous approach to building the simple documentation system with Regex route type. The disadvantage of the Regex route type is that you cannot organize the static pages in a hierarchy by creating subdirectories under the doc directory (when generating an URL for such a page, the slash directory separator will be URL-encoded making the hyperlink unusable). We will create our custom StaticRoute class that allows to fix this issue.

Moreover, the class we will create is more powerful, because it will not only recognize URLs starting with "/doc" and ending with ".html". Instead, it will recognize generic URLs, like "/help" or "/support/chapter1/introduction".

What we want to achieve:

To start, create the Route subdirectory under the module's source directory and put the StaticRoute.php file inside of it (figure 5.9).

Figure 5.9. StaticRoute.php file Figure 5.9. StaticRoute.php file

Inside that file, paste the stub code presented below:

<?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() 
    {    
    }
}

From the code above, you can see that we placed the StaticRoute class inside the Application\Route namespace (line 2).

In lines 4-9, we define some class name aliases for making the class names shorter.

In lines 12-33, we define the stub for the StaticRoute class. The StaticRoute class implements the RouteInterface interface and defines all the methods specified by the interface: factory(), match(), assemble() and getAssembledParams().

Next, let's add several protected properties and the constructor method to the StaticRoute class, as shown below:

<?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;
    }
  
    // ...
}

Above, in line 7, we define the $dirName property that is intended for storing the name of the base directory where the "static" view templates will be located. In line 10, we define the $templatePrefix class variable for storing the prefix for prepending to all view template names. Line 13 contains the $fileNamePattern variable that will be used for checking the file name.

In lines 22-29, we define the constructor method that is called on instance creation for initializing the protected properties.

Next, let's implement the factory() method for our StaticRoute custom route class. The factory() method will be called by the router for instantiating the route class:

<?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']);
    }  
}

In the code above, we see that the factory() method takes the options array as the argument (line 9). The options array may contain the options for configuring the route class. The StaticRoute class will accept the following options:

Once we parsed the options, in lines 37-41 we call the class' constructor method to instantiate and return the StaticRoute object.

The next method we add to the StaticRoute route class is the match() method:

<?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);
    }
}

In the code above, we see that the match() method takes two arguments: the HTTP request object (an instance of Zend\Stdlib\Request class) and the URL path offset. The request object is used for accessing the request URL (line 17). The path offset parameter is a non-negative integer, which points to the portion of the URL the route is matched against (line 21).

In line 24, we extract the segments from URL. Then we check if every segment is an acceptable file (directory) name (lines 27-32). If the segment is not a valid file name, we return null as a failure status.

In line 35, we calculate the path to the view template, and in lines 37-39 we check if such a file really exists and accessible for reading. This way we match the URL against the directory structure.

In lines 44-48, we prepare and return the RouteMatch object with the default parameters plus the "page" parameter containing the view template name for rendering.

To complete the implementation of our StaticRoute class, we add the assemble() and getAssembledParams() methods, that will be used for generation of URLs by route parameters. The code for these methods is presented below:

<?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;
    }
}

In the code above, we define the assemble() method, which takes the two arguments: the parameters array and the options array (line 9). The method constructs the URL by encoding the segments with URL encoding and concatenating them (line 20-26).

The method getAssembledParams() just returns the names of the parameters we used for URL generation (line 36).

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',
        ],                    
    ],
],

In line 1 of the configuration above, we define the routing rule named "static". The type parameter defines the fully qualified StaticRoute class name (line 2). In the options array, we define the base directory where the "static" pages will be placed (line 4), the template prefix (line 5), the filename pattern (line 6), and the defaults array, containing the name of the controller and the action that will serve all the static pages.

Do not forget to insert the following line to the beginning of the module.config.php class:

use Application\Route\StaticRoute;

The final step is creating the action method in the IndexController class:

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;
}

The action above is almost identical to the action we used for the Regex route. In line 4, we retrieve the page parameter from route and save it as the $pageTemplate variable. In line 11, we create the ViewModel variable container, and in line 14 we explicitly set the view template name for rendering.

To see the system in action, let's add a couple of "static" view pages: the Help page (help.phtml) and the introduction page (intro.phtml). Create the static subdirectory under the view/application/index directory of the Application module and put the help.phtml view template there:

<h1>Help</h1>

<p>
    See the help <a href="<?= $this->url('static', 
	   ['page'=>'/chapter1/intro']); ?>">introduction</a> here.
</p>

Then create the chapter1 subdirectory in the static directory and put the following chapter1/intro.phtml file in there:

<h1>Introduction</h1>

<p>
    Write the help introduction here.
</p>

Finally, you should receive the following directory structure (see figure 5.10):

Figure 5.10. Static pages Figure 5.10. Static pages

Eventually, open the following URL in your browser: http://localhost/help. The Help page should appear (see figure 5.11 for example). If you type the http://localhost/chapter1/intro URL in your browser, you should see the Introduction page (figure 5.12).

Figure 5.11. Help page Figure 5.11. Help page

Figure 5.12. Introduction page Figure 5.12. Introduction page

You can create static pages just by adding the phtml files under the static directory, and they will automatically become available to site users.

If you are stuck, you can find this complete working example inside the Hello World application.


Top