/ */ class P4Cms_Controller_Router_Route_Module extends Zend_Controller_Router_Route_Module { /** * Matches a user submitted path. Assigns and returns an array * of variables on a successful match. * * Extends Zend's module route to support ommitting the default module, * default controller and default action from the path. * * @param string $path Path used to match against this routing map. * @param bool $partial Zend provides no documentation for this param. * @return array An array of assigned values or a false on a mismatch */ public function match($path, $partial = false) { $this->_setRequestKeys(); $this->_values = array(); if (!$partial) { $path = trim($path, self::URI_DELIMITER); } else { $this->setMatchedPath($path); } // an empty path matches defaults. if ($path === '') { return $this->getDefaults(); } // break path into segments. $path = explode(self::URI_DELIMITER, $path); $defaultModule = $this->getDefault($this->_moduleKey); $defaultController = $this->getDefault($this->_controllerKey); $defaultAction = $this->getDefault($this->_actionKey); // if the first path segment does not match a valid module, // it must match a controller in the default module or a action // in the default module's default controller. if (!$this->isValidModule($path[0])) { if ($this->isValidController($path[0], $defaultModule)) { array_unshift($path, $defaultModule); } else if ($this->isValidAction($path[0], $defaultController, $defaultModule)) { array_unshift($path, $defaultController); array_unshift($path, $defaultModule); } else { return false; } } // if the second path segment doesn't match a valid controller, // assume the default controller in that module. if (!isset($path[1]) || !$this->isValidController($path[1], $path[0])) { array_splice($path, 1, 0, array($defaultController)); } // if the third path segment doesn't match a valid action, // assume the default action in that module/controller. if (!isset($path[2]) || !$this->isValidAction($path[2], $path[1], $path[0])) { array_splice($path, 2, 0, array($defaultAction)); } // verify we now reference a valid module/controller/action if ($this->isValidAction($path[2], $path[1], $path[0])) { $values = array( $this->_moduleKey => $path[0], $this->_controllerKey => $path[1], $this->_actionKey => $path[2] ); } else { return false; } // process all remaining path segments as parameters. $params = array(); for ($i = 3; $i < count($path); $i = $i + 2) { $key = urldecode($path[$i]); $value = isset($path[$i + 1]) ? urldecode($path[$i + 1]) : null; $params[$key] = (isset($params[$key]) ? (array_merge((array) $params[$key], array($value))) : $value); } $this->_values = $values + $params; return $this->_values + $this->_defaults; } /** * Determine if the given module is valid. * Alias for dispatcher's isValidModule(). * * @param string $module the (route-formatted) module to check for. * @return bool true if the module exists, false otherwise. */ public function isValidModule($module) { return $this->_dispatcher->isValidModule($module); } /** * Determine if the given controller exists in the given module. * * @param string $controller the (route-formatted) controller to check for. * @param string $module the (route-formatted) module to look in. * @return bool true if the controller exists, false otherwise. */ public function isValidController($controller, $module) { if (!$this->isValidModule($module)) { return false; } $dispatcher = $this->_dispatcher; $controller = $dispatcher->formatControllerName($controller); $className = $dispatcher->formatClassName(ucfirst($module), $controller); // verify controller is valid, return false otherwise. return class_exists($className) && is_subclass_of($className, 'Zend_Controller_Action_Interface'); } /** * Determine if the given action/module/controller represent a * valid action in the system. The identified controller must exist * and the action must match a method in the controller. * * @param string $action the (route-formatted) action to check for * @param string $controller the (route-formatted) controller containing the action * @param string $module the (route-formatted) module containing the controller * @return bool true if the action exists. */ public function isValidAction($action, $controller, $module) { if (!$this->isValidModule($module) || !$this->isValidController($controller, $module) ) { return false; } $dispatcher = $this->_dispatcher; $action = $dispatcher->formatActionName($action); $controller = $dispatcher->formatControllerName($controller); $className = $dispatcher->formatClassName(ucfirst($module), $controller); // determine whether the action exists return method_exists($className, $action); } /** * Override parent implementation to produce shorter paths. * Specifically, excludes the controller segment if it is the * default controller and there is no action segment or the * action does not match a controller. * * @param array $data An array of variable and value pairs used as parameters. * @param bool $reset Whether to reset the current params. * @param bool $encode Zend provides no documentation for this param. * @param bool $partial Zend provides no documentation for this param. * @return string Route path with user submitted parameters */ public function assemble($data = array(), $reset = false, $encode = true, $partial = false) { // move parameters to the query string if: // - any parameters contain a slash '/' // (this is a work-around for apache whereby it chokes on %2f in path info) // - any parameters are a single dot '.' // (work-around to prevent situations where uri segments containing only // a single dot are dropped; seems its not connected with the apache) $queryParams = array(); foreach ($data as $key => $value) { if (strpos((string) $key, '/') !== false || strpos((string) $value, '/') !== false || $key === '.' || $value === '.' ) { $queryParams[$key] = $value; unset($data[$key]); } } $path = parent::assemble($data, $reset, $encode, $partial); // blend given router params with current params and defaults. $params = (!$reset) ? $this->_values : array(); foreach ($data as $key => $value) { if ($value !== null) { $params[$key] = $value; } elseif (isset($params[$key])) { unset($params[$key]); } } $params += $this->_defaults; $segments = explode(self::URI_DELIMITER, $path); // while attempting to remove segments, if a non-default module is being used, // removals must begin at the second segment. $offset = 0; if (isset($params[$this->_moduleKey]) && $params[$this->_moduleKey] !== $this->_defaults[$this->_moduleKey] ) { $offset = 1; } $controller = isset($segments[0 + $offset]) ? $segments[0 + $offset] : null; $action = isset($segments[1 + $offset]) ? $segments[1 + $offset] : null; $pathParam = isset($segments[2 + $offset]) ? $segments[2 + $offset] : null; // determine if the action can be removed if (isset($action) && !$this->_isActionParamRequired($params, $action, $pathParam)) { $segments[1 + $offset] = null; $action = $pathParam; } // determine if the controller can be removed if (isset($controller) && !$this->_isControllerParamRequired($params, $controller, $action)) { $segments[$offset] = null; } // compose possibly-revised path $newSegments = array(); foreach ($segments as $segment) { if (isset($segment)) { $newSegments[] = $segment; } } $path = implode(self::URI_DELIMITER, $newSegments); // tack on any query params. if (count($queryParams)) { $path .= "?" . http_build_query($queryParams); } return $path; } /** * Get the route formatted (lower-case, dash-separated) version * of a CamelCase identifier. * * @param string $param the camel case string. * @return string the route-formatted version. */ public static function formatRouteParam($param) { return strtolower(preg_replace("/(.)([A-Z])/", "\\1-\\2", $param)); } /** * Determine if given route parameters are dispatchable. * This is accomplished by creating and populating a request * object with params and passing it to the dispatcher's * isDispatchable() method. * * @param array $params the route parameters to check. * @return bool true if the route params are dispatchable. */ protected function _isDispatchable($params) { // construct request object from route params. $request = new Zend_Controller_Request_Http; $moduleName = ''; $actionName = ''; foreach ($params as $param => $value) { $request->setParam($param, $value); if ($param === $this->_moduleKey) { $request->setModuleName($value); $moduleName = $value; } if ($param === $this->_controllerKey) { $request->setControllerName($value); } if ($param === $this->_actionKey) { $request->setActionName($value); $actionName = $value; } } $dispatcher = $this->_dispatcher; $dispatchable = $dispatcher->isDispatchable($request); // if dispatchable, we now want to determine whether the specified // action exists. if ($dispatchable and $actionName) { $controllerClass = $dispatcher->getControllerClass($request); if (!$controllerClass) { $controllerClass = $dispatcher->getDefaultControllerClass($request); } if (!$moduleName) { $moduleName = $this->_defaults[$this->_moduleKey]; } $className = $dispatcher->formatClassName(ucfirst($moduleName), $controllerClass); // if controller class doesn't exist, attempt to include it. if (!class_exists($className)) { $file = $dispatcher->getDispatchDirectory() . '/' . $dispatcher->classToFilename($controllerClass); if (Zend_Loader::isReadable($file)) { include $file; } } // verify controller is valid, return false otherwise. if (!class_exists($className) || !is_subclass_of($className, 'Zend_Controller_Action_Interface') ) { return false; } // determine whether the action exists $action = $dispatcher->getActionMethod($request); $dispatchable = method_exists($className, $action) ? true : false; } return $dispatchable; } /** * Determine if the controller parameter is required to assemble * the path. Not required if controller parameter is the default * controller and there is no action parameter or the action can * not be confused with a controller. * * @param array $params the route parameters to test against. * @param string $controller the controller to consider * @param string $action the action within the controller * @return bool true if the controller is required. */ protected function _isControllerParamRequired($params, $controller, $action) { // required if not default. if ($params[$this->_controllerKey] !== $this->_defaults[$this->_controllerKey]) { return true; } // not required if no action component. if (!isset($action)) { return false; } // required if action name matches a controller name. // we test this by setting the controller parameter to // the given action and seeing if it is dispatchable. $params[$this->_controllerKey] = $action; unset($params[$this->_actionKey]); if ($this->_isDispatchable($params)) { return true; } return false; } /** * Determine if the action parameter is required to assemble the * path. Not required if action parameter is the default * action and there are no more parameters or the following * parameter cannot be confused with a controller. * * @param array $params the route parameters to test against. * @param string $action the action to consider * @param string $pathParam the current path (or path fragment) * @return bool true if the action is required. */ protected function _isActionParamRequired($params, $action, $pathParam) { // required if not default. if ($params[$this->_actionKey] !== $this->_defaults[$this->_actionKey]) { return true; } // not required if no following parameters if (!isset($pathParam)) { return false; } // required if action name matches a controller name. // we test this by setting the controller parameter to // the given action and seeing if it is dispatchable. $params[$this->_actionKey] = $pathParam; if ($this->_isDispatchable($params)) { return true; } return false; } }