Pragmatism in the real world

Modules

One of the new features to hit the Zend Framework since 0.7 is Zend_Controller_ModuleRouter and its sibling Zend_Controller_ModuleRewriteRouter. This allows for separating out sets of controlers, models and views into their own modules. The directory structure then looks like:

application/ 
    controllers/ 
        IndexController.php 
        ArticleController.php 
    blog/ 
        controllers/ 
            IndexController.php 
        models/ 
        views/ 
    news/ 
        controllers/ 
            IndexController.php 
            ListController.php 
        models/ 
        views/ 
    models/ 
    views/ 
lib/
    Zend/
webroot/
	css/
	img/
	js/
	index.php

To set this up, you use this code in index.php:

$frontController->setControllerDirectory(array(
'default' => array('../application/controllers'),
'blog' => '../application/blog/controllers',
'news' => '../application/news/controllers'
));
$frontController->setRouter(new Zend_Controller_ModuleRouter())
->setDispatcher(new Zend_Controller_ModuleDispatcher())

The module router then maps the first parameter of the URL to the module and so then looks for a controller within that module’s controllers directory. Hence http://www.example.com/news/list/recent will map to the News_ListController::recentAction() in the ../application/news/controllers directory. If the first parameter is not a named module, then the default module is used and the first parameter is considered to be the controller instead. In this case http://www.example.com/article/list will map to ArticleController::listAction() in the ../application/controllers directory.

It’s all very clever.

One issue with the directory layout above is that the controllers within the default route will need models and views too. These are within the application directory itself an so interspersed with the module directories; feels a bit untidy to me. It makes more sense to me to create a directory called default to hold the three directories:

application/ 
    blog/ 
        controllers/ 
            IndexController.php 
        models/ 
        views/ 
    default/ 
        controllers/ 
            IndexController.php 
            ArticleController.php 
        models/ 
        views/ 
    news/ 
        controllers/ 
            IndexController.php 
            ListController.php 
        models/ 
        views/
lib/
    Zend/
webroot/
	css/
	img/
	js/
	index.php

with the set up code in index.php becoming:
To set this up, you use this code in index.php:

$frontController->setControllerDirectory(array(
'default' => array('../application/default/controllers'),
'blog' => '../application/blog/controllers',
'news' => '../application/news/controllers'
));
$frontController->setRouter(new Zend_Controller_ModuleRouter())
->setDispatcher(new Zend_Controller_ModuleDispatcher())

This now nicely keeps the MVC componts together and seems to work quite well. What I’m not sure of is how to handle an admin site. I suspect that the rewrite module router is the way to do this. I’m now into guesswork though, so will have to actually write some code in my IDE and see what happens.
I’m not sure what directory structure I want yet either.
If we take the news module:

    news/ 
        controllers/ 
            IndexController.php 
            ListController.php 
        models/ 
        views/

Do I want to put admin with it?

    news/ 
        controllers/
            admin/ 
                IndexController.php 
            IndexController.php 
            ListController.php 
        models/ 
        views/
            admin/

Or do I want a separate adminNews module? It’s likely that the models used by news and adminNews will be the same (or adminNews will use a super-set of the news model), so I can see the logic of putting the admin code within the same set of directories.

Maybe I think too much about these sort of issues!

35 thoughts on “Modules

  1. Hi, Rob!

    I think ZendController default MVC is useful, but it is not applicable for moduled applications. E.g. when I need to add new module to my site and this module injects pages to the user and admin parts, I cannot use Zend MVC.

    So I have created my custom Dispatcher that maps pair to page that will process it.

    So, module "registers" pages in any controller/module and I have the following controllers with actions:
    Blog/Index
    Blog/Atom
    Administration/ManagePosts
    Administration/ManagePost
    Administration/ManageUsers
    Administration/ManageUser
    Sitemap

    Blog module adds Blog controller, and pages ManagerPosts and ManagePost, Blog controller adds Blog/Index, Blog/Atom, Sitemap module adds Sitemap module.

  2. Mistype :-)

    Blog module adds Blog controller, pages Administration/ManagerPosts and Administration/ManagePost, Blog/Index, Blog/Atom. Sitemap module adds Sitemap module. Users module adds Administration/ManageUsers, Administration/ManageUser.

  3. Theoretically, yes. Administration is a controller and ManageUsers is an action. But practically, Administration/ManagePosts and
    Administration/ManagePost actions do the same things: load the page and call its run() method. And the page creates forms, controls, attaches actions to buttons, etc.

    Relationship between pages and Controller/Action part is stored in an associative array:
    'Administration/ManageUsers' => 'Modules_Users_Pages_ManageUsersPage'
    'Blog/Atom' => 'Modules_Blogs_Pages_AtomPage'

    So, instead of looking for the controller on disk and for the action in it my dispatcher lookups for the page that will handle the request in the array. Instantiate it and run.

    When the CMS module is on, it just "registers" new pages. So new modules can be easily distributed, added and managed.

  4. Thanks for the clarification Alex.

    I'm trying to avoid having to specifiy stuff up front as much as possible ;)

    Regards,

    Rob…

  5. Hello,
    Why not have a structure like:

    application/
      public/
        blog/
            controllers/
                IndexController.php
            models/
            views/
        default/
            controllers/
                IndexController.php
                ArticleController.php
            models/
            views/
        news/
            controllers/
                IndexController.php
                ListController.php
            models/
            views/
      private/
        blog/
            controllers/
                IndexController.php
            models/
            views/
        default/
            controllers/
                IndexController.php
                ArticleController.php
            models/
            views/
        news/
            controllers/
                IndexController.php
                ListController.php
            models/
            views/
    lib/
        Zend/
    webroot/
    	css/
    	img/
    	js/
    	index.php
    
  6. Rob — I'd do the admin versions as one of the following:

    Use subdirs within a module:

    blog/
        Admin/
            IndexController.php -- maps to  /blog/admin_index/{action}
    

    Use CamelCasing within a controller name:

    blog/
        AdminIndexController.php -- maps to /blog/admin-index or /blog/admin.index
    

    Alternately, you could use a rewrite rule that starts with 'admin/', set an 'admin' parameter in the route, and write a routeShutdown() plugin that checks for this and prepends 'admin.' or 'admin_' to the requested controller (depending on your file naming strategy).

  7. Hi Another Alex,

    One advantage of doing the division at the top level is that it's obvious where the layout files will be. Also additional "sections" of the website are separated from each other. The disadvantage of separating the admin from the front end outweighs this though.

    Rob…

  8. Matthew,

    Putting the admin within the module keeps everything together which is the biggest win. I quite like the idea of using a rewrite rule so that I can use URLs of the form /admin/{module}/{controller}/{action}.

    I'll definitely have to play with this layout.

  9. It's pretty late so I might just be acting plain stupid here, but I don't appear to have a Zend_Controller_ModuleRouter class or a Zend_Controller_ModuleDispatcher class! I’ve downloaded version 0.7.0 and just a few minutes ago downloaded the latest snapshot but these two classes don’t exist!

    Can anyone throw any light on this as I’m stuck!

    Jon

  10. Hi Jon,

    It all got changed recently :)

    The module stuff has now been rolled directly into the dispatcher and rewrite router.

    Regards,

    Rob…

  11. Hi Rob thanks for the reply.

    I've downloaded the most recent snapshot so where can i find these classes? Running the code example about i get a fatal error as the class can't be found.

  12. Hi Jon.

    I've used such code(ZF 0.8):

    setControllerDirectory(array(
    'default' => '../application/user/controllers',
    'admin' => '../application/admin/controllers'
    ));

    $frontController->setRouter(new Zend_Controller_Router_Rewrite())->setDispatcher(new Zend_Controller_Dispatcher_Standard());
    ?>

    But I don't know how to use Zend_View in this case :(

    2Rob:
    I think your article – is quite good to include it into your tutorial :)

  13. Denis,

    I haven't yet found a situation where a module would actually help me. When I do, I'll have some real-world experience and will write it up :)

    In general, I think that the concept of reuse of modules is a red-herring unless the rest of your app supports it. The main use will be organisation and I haven't yet written a ZF site so large that I need to subdivide my controllers beyond "front end" and "admin"

    Regards,

    Rob…

  14. Ok Thanks for your help.

    My directory structure is similar to this:

    [code]
    modules/news/controllers/IndexController.php
    /ListController.php
    /AdminController.php
    /blog/controllers/IndexConttoller.php
    /AdminController.php
    /etc…

    [/code]

    In response to post 9; do you know of any documentation with details of how to use the rewrite rule so that the URL will appear in the following format for all “admin controllers”:

    http://www.domain.com/admin/{module}/{controller}/{action}.

    While at the same time keeping the behavior as normal for all “non admin controllers” like so:

    http://www.domain.com/{module}/{controller}/{action}.

    Jon

  15. I’m working on a system were an application is a package containing one or more modules with views etc. Each module has three modes – index (default), edit (optional, a module user edit view) and config (optional, a module config view). The idea is to keep related modules grouped together in one application.

    URL: http://www.mysite.com/{application}/{module}/{controller}/{action}
    And then do some kind of URL mapping: /news/ -> /news/list/index/myAction

    /applications/
    Default/
    Controllers/
    IndexController.php
    Models/
    View/

    News/
    Controllers/
    List/
    IndexController.php (class name: News_List_IndexController)
    EditController.php (optional)
    ConfigController.php (optional)
    Manage/
    IndexController.php
    EditController.php (optional)
    ConfigController.php (optional)
    Models/
    View/

  16. Hello. First, thanks for your good work, tutorial with ZF :)

    I release two little applications with ZF for my company. To resolve "admin" problem, I setup a second vhost admin.mydomain.tld .
    Why ?
    I think admin and visitor have many difference with model, view, and controller methods. So Simply separate them is better.

    If I want to get some code from one to other, copy and paste.
    My 2 cent ;)

    (Sorry for my english.. I'm french)

  17. I am having a hard time with separating my controllers into modules. I have a clear need to do so, but no matter what approach i try the router is having trouble loading the files.

    Fatal error: Uncaught exception 'Zend_Exception' with message 'File "UsersIndexController.php" was

    $frontController = Zend_Controller_Front::getInstance();
    $frontController->setRouter(new Zend_Controller_Router_Rewrite())
    ->setControllerDirectory('../application/controllers')
    ->addControllerDirectory('../application/cms/controllers', 'cms')
    ->addControllerDirectory('../application/users/controllers', 'users')
    ->registerPlugin(new MyPluginAuth($auth, $acl))
    ->throwExceptions(true)
    ->setParam('acl',$acl)
    ->setParam('auth',$auth);
    $frontController->setRouter(new Zend_Controller_Router_Rewrite())->setDispatcher(new Zend_Controller_Dispatcher_Standard());

    is how my bootstrap file is set up.

  18. one other note. it is definitely reading the file, becuase when I remove the IndexController from the users/controllers dir i get this error message:

    Uncaught exception 'Zend_Controller_Dispatcher_Exception' with message 'Invalid controller specified (index)' in

    rather than

    Uncaught exception 'Zend_Exception' with message 'File "UsersIndexController.php" was not found.' in

    when it exists.

  19. I am running into one other issue. Now that I have implemented the modular aproach I am getting this error when I load a model:

    Fatal error: Uncaught exception 'Zend_Db_Table_Exception' with message 'db object does not extend Zend_Db_Adapter_Abstract'

    I tried a few naming conventions on the models. Is there another trick I am missing here?

  20. forrest.

    Have you set a default adapter for the Zend_Db_Table?

    Something like:
    $db = Zend_Db::factory($config->db->adapter, $config->db->config->asArray());
    Zend_Db_Table::setDefaultAdapter($db);

    Regards,

    Rob…

  21. thanks so much for helping me out with that. i was just refactoring an app i did using a tutorial on the zend site that assumed the reader would know where they had left out code and what to put there. it was odd, because everything else ive gotten from there was great.

    also, thanks again for your great tutorials. Ive been following them since 0.2 and they have really made the difference in my development.

  22. Some people have mentioned how to deal with models, but as far as I can tell ZF never automatically detects your model directory in the same way that it can with views anyway (although that only works with initView() from inside a controller and I don't know if I even like it).

    If I want different models with each module then I have a Zend_Controller plugin that adds to the include path using something like: "..app/".$request->getModuleName()."/models/"

    If you want to use shared models then just setup a fixed dir directly in the bootstrap as it's done in Rob's tutorial. Then maybe you could even start "modularising" your models? Your Admin_News model would extend Deafault_News and so on.

    app/
        cfg/
        logs/
        models/
            admin/
            default/
        modules/
            admin/
                controllers/
                views/
            default/
                controllers/
                views/
        tmp/
    lib/
        Zend/
    www/
        index.php
    

    I'm not really sure where to put things like headers and footers if you have two or more modules using the same ones. Right now they're just in the modules directory but it's not neat.

    There will never be a directory structure to suit everyone or even one to suit every project by the same person.

  23. I think I missed something here. When I'm trying to access the views and models, do I need to put their path somewhere? What I really want is to automatically detect the view/model locations associated with the controller in the module.

    Any thoughts?

  24. Ok, I figured out how to access my views by using the

    $view->addScriptPath('../application/moduleLocation/views');
    and executed this in my index.php

    I see the post by Kyle
    "If I want different models with each module then I have a Zend_Controller plugin that adds to the include path using something like: “..app/”.$request->getModuleName().”/models/”
    "

    I suppose there isn't a getModuleName() function already?

    I'll work on this. Thanks for your comments.

  25. [quote]
    Matthew,

    Putting the admin within the module keeps everything together which is the biggest win. I quite like the idea of using a rewrite rule so that I can use URLs of the form /admin/{module}/{controller}/{action}.

    I’ll definitely have to play with this layout.
    [/quote]

    Have you figured out how you should write this rewrite rule?
    I am currently implementing an admin-subdirectory too in my controllers directory, and /module/admin/controller/action is just more beautiful than /module/admin_controller/action…

  26. Patrick,

    Not yet. I haven't had time, so have been using two modules: default and admin.

    Regards,

    Rob…

  27. Hello everybody, my name is Damion, and I'm glad to join your conmunity,
    and wish to assit as far as possible.

  28. Hi,

    I'm struggling with the some problem. I am currently planning my CMS, and for now I think I am going to stick as well with just the two module – site & admin.

    The thing I am wondering is what to do with some of the directives in my bootstrap file.

    I am initializing Zend_Layout MVC helper with something like this:

    Zend_Layout::startMvc(array('layoutPath' => ROOT_DIR.'/application/views/layouts'));

    Which works great if you are nor using module.

    As far as I can tell, there is only one right way to do it, and that is to write your own front controller plugin that will set the appropriate path, see http://www.nabble.com/Setup-Zend_Layout-based-on-module-td16036883.html

    There is some more insight in a comment from Matthew Weier O'Phinney on http://framework.zend.com/wiki/display/ZFPROP/Zend_Layout#Zend_Layout-3.ComponentRequirements%2CConstraints%2CandAcceptanceCriteria that kind of clarifies why this Zend_Layout isn't coupled with a rewrite router.

    But, I would still be very happy if I could specify layout folder in the same way I can specify a modular structure by passing an array of modules/paths.

    Do you maybe have some other ideas?

  29. Hey,

    I've seen the topic of an admin module come up several times and I wanted to share a simple solution that worked for me.

    Create an AdminController in your default module that forwards the request to "Admin Actions."

    For example, if you have a BooksController that allows anyone to view a list of books but only a small group of admins to edit the books, you would create two methods:

    BooksController::viewAction

    BooksController::editAdminAction

    The first method would be tied to the /books/view using the normal action naming convention. Rather than using /books/edit-admin to get to the second method, the AdminController would simply forward /admin/books/edit to the second method.

    There is most likely cleaner code to do this, but here's one way that works:


    class AdminController extends Zend_Controller_Action
    {
    public function preDispatch()
    {
    $uri = $this->getUri();
    $count = count($uri);
    if ($count > 1)
    {
    $controller = $uri[1];
    $action = $count > 2 ? $uri[2] : 'index';
    $action = $this->toCamelCase($action);

    if (class_exists($controller))
    $this->_forward($action .'-admin', $controller);
    }
    }

    public function getURI()
    {
    $uri = $this->_request->getRequestUri();
    $uri = substr($uri, 1);
    $uri = preg_replace('/^(.*?)?.*?$/', '$1', $uri);
    if (strlen($uri) > 0 && substr($uri, -1, 1) == '/')
    $uri = substr($uri, 0, -1);
    return explode('/', $uri);
    }

    public function toCamelCase($str)
    {
    $str = str_replace('-', '_', $str);
    $str = explode('_', strtolower($str));
    $count = count($str);
    for ($i=1; $i<$count; $i++)
    $str[$i] = ucfirst($str[$i]);
    $str = implode("", $str);
    return $str;
    }
    }

    Hope this helps someone.

Comments are closed.