I'm building a simple CMS module for my ZendFramework application.
Currently I have all my routes in a .ini file, would it be possible to make it db-driven. Or if it's possible to build a fallback method to a DB-check if the route doesn't exists in the .ini file.
UPDATE - SOLUTION
Hope this will help somebody - Please note that I have custom My_Db_table and My_Db_Row, which I haven't posted here. So ->fetchAllActive() probably needs change to ->fetchAll(); and the getters on the row object may/may-not work, can't remember if they are custom - you'll figure it out ;-)
But Router basically check against DB if none of the other routes apply, i.e. from a .ini file. I haven't checked if this works with all the route types, but it works with the default route type. I use DB routes to point urls down to a pageController with additional params like pageId stored as a JSON string in the route_defaults cell. But you can basically use this for all kind of routes.
In bootstrap
function _initApplication ()
{
// Something taken out for simplicity
$this->bootstrap('frontcontroller');
$front = $this->getResource('frontcontroller');
$router = new My_Controller_Router_Rewrite();
$front->setRouter($router);
// Something taken out for simplicity
}
My/Controller/Router/Rewrite.php
<?php
class My_Controller_Router_Rewrite extends Zend_Controller_Router_Rewrite
{
/**
* Retrieve a named route
*
* @param string $name Name of the route
* @throws Zend_Controller_Router_Exception
* @return Zend_Controller_Router_Route_Interface Route object
*/
public function getRoute($name)
{
if (!isset($this->_routes[$name])) {
/* BEGIN - DB routes */
$routes = new Routes();
$route = $routes->getNamedRoute($name);
if($route instanceof Zend_Controller_Router_Route_Abstract) {
$this->addRoute($name, $route);
}
/* END - DB routes */
if (!isset($this->_routes[$name])) {
require_once 'Zend/Controller/Router/Exception.php';
throw new Zend_Controller_Router_Exception("Route $name is not defined");
}
}
return $this->_routes[$name];
}
/**
* Find a matching route to the current PATH_INFO and inject
* returning values to the Request object.
*
* @throws Zend_Controller_Router_Exception
* @return Zend_Controller_Request_Abstract Request object
*/
public function route(Zend_Controller_Request_Abstract $request)
{
if (!$request instanceof Zend_Controller_Request_Http) {
require_once 'Zend/Controller/Router/Exception.php';
throw new Zend_Controller_Router_Exception('Zend_Controller_Router_Rewrite requires a Zend_Controller_Request_Http-based request object');
}
if ($this->_useDefaultRoutes) {
$this->addDefaultRoutes();
}
// Find the matching route
$routeMatched = false;
foreach (array_reverse($this->_routes, true) as $name => $route) {
// TODO: Should be an interface method. Hack for 1.0 BC
if (method_exists($route, 'isAbstract') && $route->isAbstract()) {
continue;
}
// TODO: Should be an interface method. Hack for 1.0 BC
if (!method_exists($route, 'getVersion') || $route->getVersion() == 1) {
$match = $request->getPathInfo();
} else {
$match = $request;
}
if ($params = $route->match($match)) {
$this->_setRequestParams($request, $params);
$this->_currentRoute = $name;
$routeMatched = true;
break;
}
}
/* BEGIN - DB routes */
$front = Zend_Controller_Front::getInstance();
if (!$routeMatched || ($routeMatched && !Zend_Controller_Front::getInstance()->getDispatcher()->isDispatchable($request))) {
$routes = new Routes();
$dbRoutes = $routes->getRouterRoutes();
foreach ($dbRoutes as $name => $route) {
// TODO: Should be an interface method. Hack for 1.0 BC
if (method_exists($route, 'isAbstract') && $route->isAbstract()) {
continue;
}
// TODO: Should be an interface method. Hack for 1.0 BC
if (!method_exists($route, 'getVersion') || $route->getVersion() == 1) {
$match = $request->getPathInfo();
} else {
$match = $request;
}
if ($params = $route->match($match)) {
$this->_setRequestParams($request, $params);
$this->_currentRoute = $name;
$routeMatched = true;
break;
}
}
}
/* END - DB routes */
if(!$routeMatched) {
require_once 'Zend/Controller/Router/Exception.php';
throw new Zend_Controller_Router_Exception('No route matched the request', 404);
}
if($this->_useCurrentParamsAsGlobal) {
$params = $request->getParams();
foreach($params as $param => $value) {
$this->setGlobalParam($param, $value);
}
}
return $request;
}
}
Routes Db_Table model
<?php
class Routes extends My_Db_Table
{
protected $_name = 'routes';
protected $_rowClass = 'Route';
public static $primaryColumn = 'route_id';
public static $statusColumn = 'route_status';
public static $nameColumn = 'route_name';
public static $typeColumn = 'route_type';
public static $urlColumn = 'route_url';
public static $moduleColumn = 'route_module';
public static $controllerColumnn = 'route_controller';
public static $actionColumnn = 'route_action';
public static $defaultsColumnn = 'route_defaults';
public static $reqsColumnn = 'route_reqs';
public static $createdColumnn = 'route_created';
public function getRouterRoutes() {
$routes = array();
$rowset = $this->fetchAllActive();
foreach($rowset as $row) {
$routes[$row->getName()] = $row->getRouteObject();
}
return $routes;
}
public function getNamedRoute($name) {
$select = $this->select()
->where(self::$statusColumn . ' = ?', 1)
->where(self::$nameColumn . ' = ?', $name);
$rowset = $this->fetchAll($select);
foreach($rowset as $row) {
return $row->getRouteObject();
}
}
}
Route - Db_Table_row
<?php
class Route extends My_Db_Table_Row_Observable
{
public function getType() {
if(empty($this->{Routes::$typeColumn})) {
return "Zend_Controller_Router_Route";
} else {
return $this->{Routes::$typeColumn};
}
}
public function getDefaults() {
$defaults = $this->{Routes::$defaultsColumnn};
if($defaults) {
$defaults = Zend_Json::decode($defaults);
} else {
$defaults = array();
}
$defaults['module'] = $this->getModule();
$defaults['controller'] = $this->getController();
$defaults['action'] = $this->getAction();
return $defaults;
}
public function getReqs() {
$reqs = $this->{Routes::$reqsColumnn};
if($reqs) {
$reqs = Zend_Json::decode($reqs);
} else {
$reqs = array();
}
return $reqs;
}
public function getModule() {
if(empty($this->{Routes::$moduleColumn})) {
return "default";
} else {
return $this->{Routes::$moduleColumn};
}
}
public function getController() {
if(empty($this->{Routes::$controllerColumnn})) {
return "default";
} else {
return $this->{Routes::$controllerColumnn};
}
}
public function getAction() {
if(empty($this->{Routes::$actionColumnn})) {
return "default";
} else {
return $this->{Routes::$actionColumnn};
}
}
public function getRouteObject() {
$class = $this->getType();
$defaults = $this->getDefaults();
$reqs = $this->getReqs();
$route = new $class($this->getUrl(), $defaults, $reqs);
return $route;
}
}
SQL schema for routes table
CREATE TABLE IF NOT EXISTS `routes` (
`route_id` smallint(1) unsigned NOT NULL AUTO_INCREMENT,
`route_status` tinyint(1) unsigned NOT NULL,
`route_name` varchar(16) NOT NULL,
`route_type` varchar(255) NOT NULL,
`route_url` varchar(255) NOT NULL,
`route_module` varchar(32) NOT NULL,
`route_controller` varchar(32) NOT NULL,
`route_action` varchar(32) NOT NULL,
`route_defaults` varchar(255) NOT NULL,
`route_reqs` varchar(255) NOT NULL,
`route_created` datetime NOT NULL,
PRIMARY KEY (`route_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
There are three approaches that I can think of.
First is what I've successfully used in ZF1. Here's the code for the routing itself. The idea is simple: set an "alias" routing (which, in my case, simply used the ".html" suffix to differentiate it from other URLs). If the routing is found, you can get the alias from the DB and then forward the request from the target controller+action to what's defined in the DB (like here). My code isn't pretty, but it works.
Second: write your own router. You can extend the router class and simply add yout own route resolving rules there: get data from the DB, return true (and set params) if the alias is in the database.
Third: scrape the aliases table and store everything in an .ini file (or whatever cache you might be using). This is not much different from what you've implemented already and all you'd need to do would be to automate the alias scraping.