Zend Framework

Controllers in sub-directories (modules)

Details

  • Type: New Feature New Feature
  • Status: Resolved Resolved
  • Priority: Major Major
  • Resolution: Fixed
  • Affects Version/s: 0.2.0
  • Fix Version/s: 0.6.0
  • Component/s: Zend_Controller
  • Labels:
    None

Description

http://www.nabble.com/Controllers-in-subdirectories-tf2746071s16154.html
http://www.nabble.com/Controllers-in-subdirectories-tf2746071s16154.html

I've hacked the Front, Dispatcher and Router classes of the STANDARD dispatcher/router to accomplish mapping subdomains to modules but it is too messy because I had to extend 3 classes. Please sync the old router along with the RewriteRouter when controllers-in-subdirectories changes are incorporated in order to be able to over-ride the part of the code that returns the module name in a clean way in the standard router. Here are some ideas that are GENERIC enough to not break backwards compatibility:

If the _subdirectories variable is set to true, the first parameter in the basic router would be 'module', followed by controller and action parameters. Or, if the set _domain variable is subtracted from the HTTP_HOST, the remainder would be the subdomain which - in turn - would be the module. Otherwise, the code would behave as it is now - controller first, action next:

if ( $request->isSubdirectories())
{
   $module = $path[0];
   $controller = $path[1];
   //
   if ( $module )
      $controller = $module . '_' . $controller;
   //
   $action = isset($path[2]) ? $path[2] : null;
}
else
{
   $domain = $request->getDomain();
   //
   if ( $domain )             // Subdomain matching to module - more options here
   {
      $module = remainder from subtracting $domain from strtolower(HTTP_HOST) and the '.' separator
      $controller = $path[0];
      //
      ////////////////////////////////////////////////////////////////////
      // THIS ARE VERY, VERY IMPORTANT!!
      $moduleMap = $request->getModuleMap();                 // Default: 'www' => ''
      //
      // Invokes a different controller - useful for eg: developing/testing a new controller (copy), etc.
      $controllerMap = $request->getControllerMap();
      //
      // Exceptional controllers that 'modify' the $module
      $controllerToModuleMap = $request->getControllerToModuleMap();
      ////////////////////////////////////////////////////////////////////
      //
      if ( isset( $controllerMap[ $controller ] )
         $controller = $controllerMap[ $controller ];        // A different controller
      //
      if ( isset( $moduleMap[ $module ] )
         $module = $moduleMap[ $module ];                    // Maybe the 'www', no subdomain specified, etc.
      elseif ( isset( $controllerToModuleMap[ $controller ] )
         $module = $controllerToModuleMap[ $controller ];    // A different module
      //
      if ( $module )
         $controller = $module . '_' . $controller;
      //
      $action = isset($path[1]) ? $path[1] : null;
   }
   else           // Old fashioned - no subdirectories or subdomains
   {
      $controller = $path[0];
      $action = isset($path[1]) ? $path[1] : null;
   }
}

The formatControllerName method should be ehnanced to ignore directory-separators in the controller name:

public function formatControllerName($unformatted)
{
   if ( $this->_subdirectories || $this->_domain )
   {
      $unformatted = str_replace(array('-', '_', '.'), ' ', strtolower($unformatted));
      $unformatted = preg_replace('[^a-z0-9 ]', ' ', $unformatted);
      $unformatted = str_replace(' ', DIRECTORY_SEPARATOR, ucwords($unformatted));
      //
      return $unformatted . 'Controller';
   }
   //
   return ucfirst($this->_formatName($unformatted)) . 'Controller';
}

Further, the Zend::loadClass/loadFile methods could be duplicated in the Zend_Controller_Dispatcher class that would ignore directory-separators in controller names:

if ( $this->_subdirectories || $this->_domain )
{
   self::loadClass($className, $this->_directory);
   $className = str_replace( DIRECTORY_SEPARATOR, '_', $className );
}
else
   Zend::loadClass($className, $this->_directory);

Here is another enhancement to improve performance in Zend_Controller_Dispatcher::_dispatch() method that gets invoked twice (regex) - once with $performDispatch false and the next time with true:

if ( $this->_className )
{
    $className = $this->_className;        // Second pass - $performDispatch = true
    $this->_className = '';
}
else               // First pass - $performDispatch = false
{
    $className = $this->formatControllerName($action->getControllerName());
    $this->_className = $className;        // Store it to improve performance by re-using this var next-time when $performDispatch is true!
}
//
...
...

Maybe, there are better ways and ideas (refer to Rob's suggestion, too) in this regard.

Excuse me if I referred to any obsolete classes.

Activity

Hide
Shekar Reddy added a comment -

I guess, we can add the setControllerMap() to all the 3 situations and setModuleMap()/setControllerToModuleMap() to subdirectories/subdomain situations to offer the maximum features to everyone.

Show
Shekar Reddy added a comment - I guess, we can add the setControllerMap() to all the 3 situations and setModuleMap()/setControllerToModuleMap() to subdirectories/subdomain situations to offer the maximum features to everyone.
Hide
Michal Minicki added a comment -

Added code tags to make the issue more readable. And assigning to Matthew.

Show
Michal Minicki added a comment - Added code tags to make the issue more readable. And assigning to Matthew.
Hide
Shekar Reddy added a comment -

Modules are subfolders/subdomains, controllers are classes and actions are methods. Just eliminated some redundancy and re-orgnaized the code to offer maximum features to everyone:

...
...

/////////////////////////////////////////////////////////////////////////////
// CUSTOM MAPPING - THESE ARE VERY IMPORTANT
/////////////////////////////////////////////////////////////////////////////
// Invokes a different controller - useful for eg: developing/testing a new controller (copy), etc.
$controllerMap = &$request->getControllerMap();
//
$moduleMap = &$request->getModuleMap();                 // Default: 'www' => ''
//
// Exceptional controllers that 'modify' the $module
$controllerToModuleMap = &$request->getControllerToModuleMap();
/////////////////////////////////////////////////////////////////////////////
//
$module = '';
//
if ( $request->isSubdirectories())      // Subdirectory matching to module
{
   $module = $path[0];
   $controller = $path[1];
   $action = isset($path[2]) ? $path[2] : null;
}
else
{
   // Old fashioned - no subdirectories or subdomains
   $controller = $path[0];
   $action = isset($path[1]) ? $path[1] : null;
   //
   // Are modules supported through sub-domains?
   $domain = $request->getDomain();
   //
   if ( $domain )                      // Subdomain matching to module
   {
      $module = remainder from subtracting $domain and the '.' separator from strtolower(HTTP_HOST)
   }
}
//
if ( isset( $controllerMap[ $controller ] )
   $controller = $controllerMap[ $controller ];           // A different controller
//
if ( isset( $moduleMap[ $module ] )
   $module = $moduleMap[ $module ];                       // Maybe the 'www', no subdomain specified, etc.
elseif ( isset( $controllerToModuleMap[ $controller ] )
   $module = $controllerToModuleMap[ $controller ];       // A different module
//
if ( $module )
   $controller = $module . '_' . $controller;

...
...
Show
Shekar Reddy added a comment - Modules are subfolders/subdomains, controllers are classes and actions are methods. Just eliminated some redundancy and re-orgnaized the code to offer maximum features to everyone:
...
...

/////////////////////////////////////////////////////////////////////////////
// CUSTOM MAPPING - THESE ARE VERY IMPORTANT
/////////////////////////////////////////////////////////////////////////////
// Invokes a different controller - useful for eg: developing/testing a new controller (copy), etc.
$controllerMap = &$request->getControllerMap();
//
$moduleMap = &$request->getModuleMap();                 // Default: 'www' => ''
//
// Exceptional controllers that 'modify' the $module
$controllerToModuleMap = &$request->getControllerToModuleMap();
/////////////////////////////////////////////////////////////////////////////
//
$module = '';
//
if ( $request->isSubdirectories())      // Subdirectory matching to module
{
   $module = $path[0];
   $controller = $path[1];
   $action = isset($path[2]) ? $path[2] : null;
}
else
{
   // Old fashioned - no subdirectories or subdomains
   $controller = $path[0];
   $action = isset($path[1]) ? $path[1] : null;
   //
   // Are modules supported through sub-domains?
   $domain = $request->getDomain();
   //
   if ( $domain )                      // Subdomain matching to module
   {
      $module = remainder from subtracting $domain and the '.' separator from strtolower(HTTP_HOST)
   }
}
//
if ( isset( $controllerMap[ $controller ] )
   $controller = $controllerMap[ $controller ];           // A different controller
//
if ( isset( $moduleMap[ $module ] )
   $module = $moduleMap[ $module ];                       // Maybe the 'www', no subdomain specified, etc.
elseif ( isset( $controllerToModuleMap[ $controller ] )
   $module = $controllerToModuleMap[ $controller ];       // A different module
//
if ( $module )
   $controller = $module . '_' . $controller;

...
...
Hide
Matthew Weier O'Phinney added a comment -

The changes to the dispatcher are now in subversion, so now we can deal with routing. My idea is to simply pass an argument to the front controller using addParam(), check for this in the router, and, if present, choose to use the module schema:

$front->addParam('useModules', true);

in the router:

public function route (....)
{
    // ...

    if ($this->getParam('useModules')) {
        // first param is module; grab and set in request
    }

    // continue processing as normal, grabbing controller and then action
}

In the rewrite router, we can do the same thing in addDefaultRoutes().

If this makes sense, I'll proceed.

Show
Matthew Weier O'Phinney added a comment - The changes to the dispatcher are now in subversion, so now we can deal with routing. My idea is to simply pass an argument to the front controller using addParam(), check for this in the router, and, if present, choose to use the module schema:
$front->addParam('useModules', true);
in the router:
public function route (....)
{
    // ...

    if ($this->getParam('useModules')) {
        // first param is module; grab and set in request
    }

    // continue processing as normal, grabbing controller and then action
}
In the rewrite router, we can do the same thing in addDefaultRoutes(). If this makes sense, I'll proceed.
Hide
Shekar Reddy added a comment -

Perfect!

Thanks!

Show
Shekar Reddy added a comment - Perfect! Thanks!
Hide
Matthew Weier O'Phinney added a comment -

Final documentation, tests, and routing changes done in revision 2182. The front controller/router parameter 'useModules' will indicate that the module should be the first item in the path.

Show
Matthew Weier O'Phinney added a comment - Final documentation, tests, and routing changes done in revision 2182. The front controller/router parameter 'useModules' will indicate that the module should be the first item in the path.

People

Vote (0)
Watch (2)

Dates

  • Created:
    Updated:
    Resolved: