views:

3753

answers:

5

I am working on implementing Zend Framework within an existing project that has a public marketing area, a private members area, an administration site, and a marketing campaign management site. Currently these are poorly organized with the controller scripts for the marketing area and the members area all being under the root of the site and then a separate folder for admin and another folder for the marketing campaign site.

In implementing the Zend Framework, I would like to create be able to split the controllers and views into modules (one for the members area, one for the public marketing area, one for the admin site, and one for the marketing campaign admin site) but I need to be able to point each module to the same model's since all three components work on the same database and on the same business objects.

However, I haven't been able to find any information on how to do this in the documentation. Can anyone help with either a link on how to do this or some simple instructions on how to accomplish it?

+3  A: 

What I do is keep common classes in a "library" directory outside of the modules hierarchy. Then set my INCLUDE_PATH to use the "models" directory of the respective module, plus the common "library" directory.

docroot/
    index.php
application/
    library/    <-- common classes go here
    default/
        controllers/
        models/
        views/
    members/
        controllers/
        models/
        views/
    admin/
        controllers/
        models/
        views/
. . .

In my bootstrap script, I'd add "application/library/" to the INCLUDE_PATH. Then in each controller's init() function, I'd add that module's "models/" directory to the INCLUDE_PATH.

edit: Functions like setControllerDirectory() and setModuleDirectory() don't add the respective models directories to the INCLUDE_PATH. You have to do this yourself in any case. Here's one example of how to do it:

$app = APPLICATION_HOME; // you should define this in your bootstrap
$d = DIRECTORY_SEPARATOR;
$module = $this->_request->getModuleName(); // available after routing
set_include_path(
  join(PATH_SEPARATOR,
    array(
      "$app{$d}library",
      "$app{$d}$module{$d}models",
      get_include_path()
    )
  )
);

You could add the "library" to your path in the bootstrap, but you can't add the "models" directory for the correct module in the bootstrap, because the module depends on routing. Some people do this in the init() method of their controllers, and some people write a plugin for the ActionController's preDispatch hook to set the INCLUDE_PATH.

Bill Karwin
Is there any reason that you don't use addControllerDirectory() rather than adding a value to the INCLUDE_PATH?
Noah Goodrich
addControllerDirectory() doesn't add the respective models directory to your include path, as far as I know. See examples in my edit above.
Bill Karwin
+2  A: 

This can also be accomplished through a naming convention to follow Zend_Loader. Keep your model files in the models folder under their module folder. Name them as Module_Models_ModelName and save them in a file name ModelName.php in the models folder for that module. Make sure the application folder is in your include path and assuming you are using Zend_Loader for auto loading, you can then just reference the models by their class name.

This has the advantage of keeping your model code grouped in with the actual module it is for. This keeps the module contained within a single folder structure which helps encourage encapsulation. This will also help in the future if you need to port the module to another project.

D-Rock
A: 

D-Rock, I like your approach but I'm having a problem with case sensitivity. Zend_Loader expects my models to have directories that start with uppercase names with a path like Module/Models/ModelName.php but the dispatcher is looking for controllers inside of lowercase named module directories like modules/controllers/ControllerName.php

So the dispatcher is throwing errors like 'Undefined index: default' because the actual directory name is Default with a capital D. How do you get around this?

This could be posted as a separate question or as a comment to D-Rock. StackOverflow is not a conversational or thread-based answer wiki. Each question has answers but additional questions should not contain other questions.
Noah Goodrich
He obviously cannot comment yet since he only has 1 rep.
leek
A: 

I'm having the same problem. Bill's answer doesn't fit for me - cos i tend to divide my modules, not by 'who is seeing them', but by 'what they do'. E.g a 'forum module' might be managed by both admin and public. I'm trying to have front end modules, like admin, members , public - but these then use other modules like 'forum/validatepost', 'forum/show users personal info'. If anyone could shed light on how they protect a back-end module from the public , then that would be handy. I guess ACL may be the key but it still makes me nervous having access controlled by objects as opposed 'file system/.htaccess' etc.

To answer PHPoet's question : (i) Paths to module's controller directories can be specified by calls to front controller: e.g see : "12.11.2. Specifying Module Controller Directories" (Zend Framework Docs)

(ii) Paths to views can be set using ViewRenderer (Controller Action Helper) e.g. see: 'Example 12.12. Choosing a Different View Script' (Zend Framework Docs)

By playing around its possible to alter the default paths to views and controllers, thus freeing up your autoloader to run as normal.

(I have not looked into the way autoloader works, but it would make sense for it to have some mapper system to solve this kind of issue.)

+1  A: 

I just built this custom Action Helper for the problem you describe:

<?php

class My_Controller_Action_Helper_GetModel extends Zend_Controller_Action_Helper_Abstract
{
  /**
   * @var Zend_Loader_PluginLoader
   */
  protected $_loader;

  /**
   * Initialize plugin loader for models
   * 
   * @return void
   */
  public function __construct()
  {
    // Get all models across all modules
    $front = Zend_Controller_Front::getInstance();
    $curModule = $front->getRequest()->getModuleName();

    // Get all module names, move default and current module to
    //  back of the list so their models get precedence
    $modules = array_diff(
      array_keys($front->getDispatcher()->getControllerDirectory()),
      array('default', $curModule)
    );
    $modules[] = 'default';
    if ($curModule != 'default') {
      $modules[] = $curModule;
    }

    // Generate namespaces and paths for plugin loader
    $pluginPaths = array();
    foreach($modules as $module) {
      $pluginPaths[ucwords($module)] = $front->getModuleDirectory($module) . '/models';
    }

    // Load paths
    $this->_loader = new Zend_Loader_PluginLoader($pluginPaths);
  }

  /**
   * Load a model class and return an object instance
   * 
   * @param  string $model 
   * @return object
   */
  public function getModel($model)
  {
    $class = $this->_loader->load($model);
    return new $class;
  }

  /**
   * Proxy to getModel()
   * 
   * @param  string $model 
   * @return object
   */
  public function direct($model)
  {
    return $this->getModel($model);
  }
}

So in your Bootstrap.php:

Zend_Controller_Action_HelperBroker::addPrefix('My_Controller_Action_Helper');

And in any of your controllers:

<?php

class IndexController extends Zend_Controller_Action 
{
  public function indexAction() 
  {
    $model = $this->_helper->getModel('SomeModel');
  }
}

And this will allow your access to models in any controller across all modules.

jakemcgraw