Page 1 of 1

Zend Framework and MVC in general

Posted: Fri Nov 17, 2006 10:57 am
by Luke
In the past, I've used the __construct method of my controllers to initialize things I'll need throughout the controller like so...

Code: Select all

public function __construct()
{
    $this->user 	= Zend::registry('user');
    $this->config 	= Zend::registry('config');
    $this->session    = Zend::registry('session');

    $this->view 	= Zend::registry('view');
    $this->view->assign('title', 'My Website');
    $this->view->assign('username', $this->user->getSafe('username'));

    parent::__construct();
}
In the Zend Framework incubator, the __construct function for the Zend_Controller_Action class is final, so I can't do this... I'm assuming that it is in some way bad practice, or they wouldn't have mad the __construct method final. Is this bad practice? What do you guys do in your controllers?

Could somebody post an example controller of theirs (preferably a Zend_Controller_Action if you have it) so I can see how far off my idea of a controller is... ?

also... I've noticed this method in the Zend_Controller_Action class...

Code: Select all

final protected function _forward($controllerName, $actionName, $params=array())
What is this for... how do I use it? Thanks a lot!

Posted: Fri Nov 17, 2006 1:45 pm
by John Cartwright
In the past, I've used the __construct method of my controllers to initialize things I'll need throughout the controller like so...

Code: Select all

public function __construct()
{
    $this->user         = Zend::registry('user');
    $this->config       = Zend::registry('config');
    $this->session    = Zend::registry('session');

    $this->view         = Zend::registry('view');
    $this->view->assign('title', 'My Website');
    $this->view->assign('username', $this->user->getSafe('username'));

    parent::__construct();
}
I inherit an application controller, to which I initialize all my required layers.

Code: Select all

abstract class Application_Controller extends Zend_Controller_Action 
	{
		protected $action;
		protected $controller;
		protected $objects;
		protected $namespace = array('model', 'view');	
		
		public function initialize()
		{
			$this->action = $this->_action->getActionName();
			$this->controller = $this->_action->getControllerName();
			
			foreach ($this->namespace as $name) {
				$this->object = $this->formatObjectName($name);
				if (!$this->fileExists()) {
					throw new Zend_Exception($name .' object does not exist');
				}
				$this->hookObject($name);
			}

			return $this->getActiveObjects();
		}			

		protected function hookObject($type)
		{
			switch ($type) {
				case 'view'  : $this->hookViewLayer();  break;
				case 'model' : $this->hookModelLayer(); break;
			}
		}
		
		....
		....
	}

	class IndexController extends Application_Controller
	{
		public function IndexAction() 
		{
			$object = $this->initialize();

			$object['view']-> ...
                        $object['model']-> ...
		}
	}
I'm sure there is a better way of doing it, so if anyone knows do tell. If you need further explanation on that, let me know.

As for this part in your controller,

Code: Select all

$this->user         = Zend::registry('user');
    $this->config       = Zend::registry('config');
    $this->session    = Zend::registry('session');
I prefer to load plugins in my index php, and have that handle those kinds of requests. You want to keep your controller logic as clean as possible.

Code: Select all

/*
	 * Load Plugins
	*/
	$controller->registerPlugin(new Application_Plugin_Configuration());
	$controller->registerPlugin(new Application_Plugin_DbConnection());
	$controller->registerPlugin(new Application_Plugin_Session());	
	$controller->registerPlugin(new Application_Plugin_Access());
	$controller->registerPlugin(new Application_Plugin_Layout());
I suggest you read upon how plugins work, but here is one I use to load configuration objects into my registry

Code: Select all

class Application_Plugin_Configuration extends Zend_Controller_Plugin_Abstract
	{
		public function preDispatch($controller)
		{
			Zend::register('config', new Zend_Config(Zend_Config_Ini::load('Config/config.ini', 'config')));
			
			return $controller;
		}
	}
also... I've noticed this method in the Zend_Controller_Action class...

Code: Select all

final protected function _forward($controllerName, $actionName, $params=array())

What is this for... how do I use it? Thanks a lot!
This is used to forward a request to another action (in the same request unlike _redirect()). In your application controller, I have a wrapper method for zend's built in forwarder. I do something similar to

example,

Code: Select all

class IndexController extends Application_Controller
	{
		public function IndexAction() 
		{
			$this->forward('Index', 'ForwardedAction', array('foo' => 'bar'));
		}

                public function ForwardedAction()
		{
			echo 'hi';
		}
	}

Posted: Fri Nov 17, 2006 1:51 pm
by Luke
Jcart... right on... you answered both of those questions perfectly... you're the man. Especially the plugin stuff... that's exactly what I was looking for!

One more question though... what types of situations would need you to forward to another action? Examples?

Thanks a lot man!

Posted: Fri Nov 17, 2006 1:57 pm
by John Cartwright
Typically situations where you need multi step processing of data, often enough you can simply do this in one method. Other times, it makes sense to seperate them (especially if they are for different purposes).

Code: Select all

class SurveyController
{
   public function StepOneAction()
   {
      if ( data process validation )
      {
         $objects['model']->InsertStepOne( ... );

         $this->forward('SurveyController', 'StepTwo', array( ? ));
      }
   }
}
Can't really think of a good example. More often than not, a simply redirect() should suffice.

Posted: Sat Nov 18, 2006 8:00 pm
by Luke
I do have another questions... what is this hooks business? It looks interesting...

Code: Select all

public function initialize()
                {
                        $this->action = $this->_action->getActionName();
                        $this->controller = $this->_action->getControllerName();
                       
                        foreach ($this->namespace as $name) {
                                $this->object = $this->formatObjectName($name);
                                if (!$this->fileExists()) {
                                        throw new Zend_Exception($name .' object does not exist');
                                }
                                $this->hookObject($name);
                        }

                        return $this->getActiveObjects();
                }                     

                protected function hookObject($type)
                {
                        switch ($type) {
                                case 'view'  : $this->hookViewLayer();  break;
                                case 'model' : $this->hookModelLayer(); break;
                        }
                }

Posted: Sun Nov 19, 2006 12:17 am
by John Cartwright
Let me just give you an example,

Code: Select all

protected function hookModelLayer() 
{
	$this->objects['model'] = new $this->object();
	
	if (!Zend::isRegistered('database')) {
		throw new Zend_Exception('No database object found');
	}
	
	$this->objects['model']->db = Zend::registry('database');
}

Posted: Sun Nov 19, 2006 12:11 pm
by Maugrim_The_Reaper
I'm building a project on top of the ZF (how better to learn it). Notably I use the current SVN and the incubator components, so what I have is strictly for the revised MVC components. The current basic Zend_Controller_Action subclass I added:

Code: Select all

<?php
/**
 * LICENSE
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to Astrum@Astrum.com so we can send you a copy immediately.
 *
 * @category   Astrum
 * @package    Astrum_Controller
 * @copyright  Copyright (c) 2006 Pádraic Brady (http://blog.quantum-star.com)
 * @license    http://www.gnu.org/copyleft/gpl.html     GNU General Public License
 */

/** Astrum_Controller_Action_Exception */
require_once 'Astrum/Controller/Action/Exception.php';

/** Zend_Controller_Action **/
require_once 'Zend/Controller/Action.php';

/**
 * Extends Zend_Controller_Action for custom Action tasks and setup.
 *
 * @category   Astrum
 * @package    Astrum_Controller
 * @copyright  Copyright (c) 2006 Pádraic Brady (http://blog.quantum-star.com)
 * @license    http://www.gnu.org/copyleft/gpl.html     GNU General Public License
 */
class Astrum_Controller_Action extends Zend_Controller_Action
{

    /**
     * Holds a Zend_View object.
     *
     * @var Zend_View
     */
    protected $_view = null;

    /**
     * Holds a Zend_Session object.
     *
     * @var Zend_Session
     */
    protected $_session = null;

    /**
     * Holds a Zend_Registry object.
     *
     * @var Zend_Registry
     */
    protected $_registry = null;

    /**
     * Holds a Zend_Filter_Input object encapsulating $_POST.
     *
     * @var Zend_Filter_Input
     */
    protected $_post = null;

    /**
     * Holds a Zend_Filter_Input object encapsulating $_GET.
     *
     * @var Zend_Filter_Input
     */
    protected $_get = null;

    /**
     * Initialise the custom Controller and
     * assign variables for use in Action methods.
     *
     * @return none
     * @access public
     */
    public function init()
    {
        if($this->getInvokeArg('post'))
        {
            $this->_post = $this->getInvokeArg('post');
        }
        if($this->getInvokeArg('get'))
        {
            $this->_get = $this->getInvokeArg('get');
        }
        $this->_view = $this->getInvokeArg('view');
        $this->_session = $this->getInvokeArg('session');
        $this->_registry = $this->getInvokeArg('registry');

        /*
         * Additional settings for View
         */
        $this->_view->URLROOT = $this->getRequest()->getBaseUrl();
    }

}
Here's what my current (damn messy!) bootstrap index.php file looks like:

Code: Select all

<?php
/**
 * Bootstrap file for Astrum Futura
 * 
 * For current SVN of the Zend Framework.
 */

/*
 * The basics...
 */
error_reporting(E_ALL);
ini_set('display_errors', 1);
date_default_timezone_set('Europe/London');

/*
 * Setup the include_path to the ZF library.
 * We set the incubator first so the
 * incubator classes are loaded in preference
 * to core ZF classes where two versions exist.
 *
 * When 0.21 is released, the MVC classes in
 * Incubator will move to the core library.
 */
set_include_path(
    './library/incubator/library'
    . PATH_SEPARATOR . './library'
    . PATH_SEPARATOR . './library/extra'
    . PATH_SEPARATOR . './application/tables'
    . PATH_SEPARATOR . get_include_path()
);
require_once 'Zend.php';

/*
 * Require essential classes
 */
require_once 'Zend/Registry.php';
require_once 'Zend/Controller/Front.php';
require_once 'Zend/Controller/RewriteRouter.php';
require_once 'Zend/View.php';
require_once 'Zend/Session.php';
require_once 'Zend/Config/Ini.php';
require_once 'Quantum/Db.php'; // custom ORM lite solution
require_once 'Quantum/Db/Access.php';

/*
 * Create any objects needed for use in
 * controllers. Can avoid using a static Registry
 * (increases coupling) with this method (cleaner).
 *
 * Such objects are passed into the Controller
 * layer as Invoked Arguments (added to
 * Front Controller using setParam()
 */

/*
 * Create Registry
 */
$registry = Zend_Registry::getInstance();
/*
 * Create View object
 */
$view = new Zend_View();
$view->setScriptPath('./application/views');
/*
 * Create Session object
 * Expire authentication flag after 5 minutes
 */
$session = new Zend_Session();
$session->setExpirationSeconds(300, 'authenticated');

/*
 * Create DB Config object
 */
$db_config = new Zend_Config_Ini('./data/db.ini', 'local');
$registry->set('dbconfig', $db_config);

/*
 * Get database connection and set on
 * Registry
 */
require_once 'adodblite/adodb.inc.php';
$db = Quantum_Db::factory($db_config);
$dao = Quantum_Db_Access::getInstance($db);
$registry->set('dbaccess', $dao);

/*
 * Instantiate a RewriteRouter
 *
 * Disable default behaviour of allowing
 * arbitrary parameters appended to URI
 */
$router = new Zend_Controller_RewriteRouter();
$router->removeRoute('default');

/*
 * @todo Set applicable routes (move to new file when large enough)
 */
$routes = array();
$routes['astrum_default'] = new Zend_Controller_Router_Route(':controller/:action', array('controller' => 'index', 'action' => 'index'));
$routes['astrum_login'] = new Zend_Controller_Router_Route('login', array('controller' => 'login', 'action' => 'index'));
$routes['astrum_signup'] = new Zend_Controller_Router_Route('signup', array('controller' => 'signup', 'action' => 'index'));
$router->addRoutes($routes);


/*
 * On my platform, I need to set the BaseURL for ZF 0.20
 * RewriteBase is assumed to be $_SERVER['PHP_SELF'] after
 * removing the trailing "index.php" string.
 *
 * PHP_SELF can be user manipulated. Avoided using SCRIPT_NAME
 * or SCRIPT_FILENAME because they may differ depending on SAPI
 * being used.
 */
$base_url = substr($_SERVER['PHP_SELF'], 0, -9);

/*
 * Require custom Zend_Controller_Action subclass
 */
require_once('Astrum/Controller/Action.php');

/*
 * Setup and run the Front Controller
 *
 * Set Controller Dir, add the RewriteRouter, set
 * params to be passed to Controller, set a custom
 * custom Base URL, dispatch the request and get
 * the resulting Response object.
 */
$controller = Zend_Controller_Front::getInstance();

/*
 * Create filters and pass to Controller
 * This will disable the GET/POST superglobals
 * and force access through the filter object
 */
if(isset($_POST) and !empty($_POST))
{
    require_once 'Zend/Filter/Input.php';
    $controller->setParam('post', new Zend_Filter_Input($_POST));
}
if(isset($_GET) and !empty($_GET))
{
    require_once 'Zend/Filter/Input.php';
    $controller->setParam('get', new Zend_Filter_Input($_GET));
}

/*
 * Add other common objects to be
 * passed to Controller.
 */
$controller ->setParam('view', $view)
            ->setParam('session', $session)
            ->setParam('registry', $registry)
            ->setParam('dao', $dao);

$response = $controller
        ->setControllerDirectory('./application/controllers')
        ->setRouter($router)
        ->setBaseUrl($base_url)
        ->dispatch();

/*
 * By default Exceptions are not displayed
 * That won't do during development.
 * Remove this in a live environment!
 *
 * $response->renderExceptions(true); will not
 * work, it's a bit broken and was fixed in SVN for
 * next release. Until then...manually echo exception
 */
if($response->isException())
{
    echo $response->getException();
    exit(0); // Stop here - 
}

/*
 * Echo the response (with headers) to client
 * Zend_Controller_Response_Http implements
 * __toString().
 */
echo $response;
Notably, it's worth avoiding the Zend::register() method since it's an unnecessary static call. You can directly create a Registry Singleton and add it as a Controller parameter (invoked arg) which can be access in your custom Controller class and set to protected properties for use in all Action methods. The same goes for any common use object - the Registry is then limited to storing data/objects between Action methods.

The _forward method specifies that you wish to forward from the current Action to a second Action. Using it you can chain Actions one after the other. This comes in useful when one or more Actions share a common Action dedicated to building a specific View. Once each Action has done it's small part, the end Action (forwarded to) renders a final View. Sorry, no definitive use case yet - but I'm currently using it to forward invalid Controller calls to the IndexController.

A sample Controller based on my setup (the LoginController handles authentication):

Code: Select all

<?php
/**
 * LICENSE
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to redux@redux.com so we can send you a copy immediately.
 *
 * @category   Astrum
 * @package    Astrum_Controller
 * @copyright  Copyright (c) 2006 Pádraic Brady (http://blog.quantum-star.com)
 * @license    http://www.gnu.org/copyleft/gpl.html     GNU General Public License
 */

/**
 * Login Controller
 *
 * @category   Astrum
 * @package    Astrum_Controller
 * @copyright  Copyright (c) 2006 Pádraic Brady (http://blog.quantum-star.com)
 * @license    http://www.gnu.org/copyleft/gpl.html     GNU General Public License
 */
class LoginController extends Astrum_Controller_Action
{

    /**
     * Forward invalid routes to the IndexController
     *
     * @access public
     */
    public function norouteAction()
    {
        $this->_forward('index', 'index');
    }

    /**
     * Process the form data and perform authentication.
     *
     * @access public
     * @todo Utilise feyd's SHA256 class for hashing passwords
     * @todo Implement challenge/response system
     */
    public function indexAction()
    {
        /*
         * Check request type, and redisplay form if not POST
         */
        if($this->getRequest()->getMethod() !== 'POST' || !isset($this->_post))
        {
            $this->_forward('index', 'index');
            return;
        }
        
        require_once 'User.php';
        require_once 'Astrum/Auth/Adapter/Simple.php';
        $credentials = new Astrum_User;
        $credentials->name = $this->_post->getRaw('astrum_form_login_user');
        $credentials->password = sha1($this->_post->getRaw('astrum_form_login_pass'));
        $authenticator = new Astrum_Auth_Adapter_Simple;

        // if valid token returned, authenticator found user valid and set a "authenticated" session var
        // Per bootstrap, authenticated session var expires after 5 minutes since last request
        $token = $authenticator->authenticate($credentials, new Astrum_User, $this->_session);

        if(!$token->isValid())
        {
            $this->getResponse()->appendBody(
                '<strong>' . $token->getMessage() . '</strong>'
            );
            $this->_forward('index', 'index');
            return;
        }
        
        $user = $token->getIdentity();
        $this->_view->user = $user->asArray();
        $this->getResponse()->appendBody(
            $this->_view->render('login_index.tpl.html')
        );
    }

}
Quick note, the init() function is called by the ctor of the Zend_Controller_Action abstract. No need to override the ctor.

Again, this is based on the current SVN of the ZF, particularly the newer incubator classes.

Posted: Mon Nov 20, 2006 6:19 pm
by John Cartwright
I'm having a tough time figuring out where your getting this function from

Code: Select all

$controller ->setParam('view', $view)
            ->setParam('session', $session)
            ->setParam('registry', $registry)
            ->setParam('dao', $dao);
care to explain please?

Posted: Mon Nov 20, 2006 6:21 pm
by Luke
It's in the SVN... this code is not in the Zend Framework yet.

Posted: Tue Nov 21, 2006 2:51 am
by Maugrim_The_Reaper
I'm working off the SVN version. Mainly because the ZF is undergoing active API changes and it makes sense to adopt changes early rather than working through a long list of them in the release as well as the inevitable refactoring that would require. Also, the SVN (all except the Session component for some reason) are unit tested within a short time of being committed so it stays reasonably stable. Personally the current practice of writing unit tests after writing a component isn't ideal - it's already led to some API changes after releases.

On another tack in case it's confusion. Values can be passed the to Front Controller using setParam(), which are accessible inside an Action Controller by calling getInvokeArg() or getInvokeArgs(). The naming differs on both sides. In the Action Controller getParam() grabs URI passed parameters (the variables you allow in routes) from the current Request object. Would be nice to keep the naming consistent on both sides... Might yet happen, there's a current proposal to do some work on the Request object itself...

Posted: Sun Nov 26, 2006 1:09 pm
by John Cartwright
I've never understood setting custom routes..

Code: Select all

$routes = array();
$routes['astrum_default'] = new Zend_Controller_Router_Route(':controller/:action', array('controller' => 'index', 'action' => 'index'));
$routes['astrum_login'] = new Zend_Controller_Router_Route('login', array('controller' => 'login', 'action' => 'index'));
$routes['astrum_signup'] = new Zend_Controller_Router_Route('signup', array('controller' => 'signup', 'action' => 'index'));
$router->addRoutes($routes);
Whats the point? Why not allow the default router handle this on it's own?

Posted: Sun Nov 26, 2006 1:55 pm
by Christopher
Maugrim_The_Reaper wrote:On another tack in case it's confusion. Values can be passed the to Front Controller using setParam(), which are accessible inside an Action Controller by calling getInvokeArg() or getInvokeArgs(). The naming differs on both sides. In the Action Controller getParam() grabs URI passed parameters (the variables you allow in routes) from the current Request object. Would be nice to keep the naming consistent on both sides... Might yet happen, there's a current proposal to do some work on the Request object itself...
A lot of this is because they are working from a proposal process rather than from the ideas of a core group of architects. This the the bazaar rather than the cathedral ... and it produces a much messier process and opens the code up for many more complaints, but I believe it the long run it produces more useful code. Compare this to RoR which is an architected thing of beauty, but not a flexible as first glance implies and is growing warts as it ages.

I would certainly propose the naming / name change that you think is proper. It may have been overlooked simply because they have full plates. It is the kind of input that they want.
Jcart wrote:Whats the point? Why not allow the default router handle this on it's own?
I think you are seeing some leftover design from the original release. Whether they let go of it or not -- time will tell. It is a flexible design in that you can replace both the route generation and route resolver, but whether that is necessary flexiblity or a good design it is hard to tell at this point. I tend to agree with you and think a simpler extensible Router would be a better solution for the reality of usage.