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.
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:
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.
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:
The StaticRoute
class should be insertable to the route stack (to SimpleRouteStack
or to TreeRouteStack
) and usable together with other route types.
The route class should recognize generic URLs, like "/help" or "/introduction".
The route class should match the URL against the directory structure. For example, if the URL is "/chapter1/introduction", then the route should check if the corresponding view template file <base_dir>/chapter1/introduction.phtml exists and is readable, and if so, report match. If the file does not exist (or not readable), return the failure status.
The route class should check the URL for acceptable file names using a regular expression. For example, the file name "introduction" is acceptable, but the name "*int$roduction" is not. If the file name is not acceptable, the failure status should be returned.
The route should be able to assemble the URL string by route name and parameters.
To start, create the Route subdirectory under the module's source directory and put the StaticRoute.php file inside of it (figure 5.9).
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:
dir_name
- the base directory where to store all "static" view templates.template_prefix
- the prefix to prepend to all template names.filename_pattern
- the regular expression for checking the file names.defaults
- parameters returned by router by default.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):
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).
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.