Skip to end of metadata
Go to start of metadata

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[

Zend Framework: Zend MVC Components Component Proposal

Proposed Component Name Zend MVC Components
Developer Notes http://framework.zend.com/wiki/display/ZFDEV/Zend MVC Components
Proposers Matthew Weier O'Phinney
Revision 1.1 - 22 September 2006: Created (wiki revision: 12)

Table of Contents

1. Overview

There are many proposed changes. The most important and far-reaching is the creation of a variety of Request classes and interfaces that will be used to inform the entire controller workflow of the request environment. This change will allow better unit testing of components (as the controllers are no longer dependent on the web environment in which they reside); the ability for the various objects in the controller chain to pass information to one another; and the ability to use the controller chain within non-web environments, such as the CLI or a GUI.

Some discussion centered around removing the need for the Router and/or Dispatcher. We have decided to keep these, but have them receive and return Request objects as the common currency instead of the Dispatcher Token. By doing so, we have a common environment that all objects share during execution.

Another discussion regarded the optional passing of a Zend_Registry object through the controller chain. However, nobody stepped forth to show (1) a solid use case where this would be necessary, and (2) a reason this couldn't be done via other means. One method, proposed by Michael Sheakoski and revised by Simon Mundy, would add a method to the Front Controller to allow passing optional invokation parameters to other objects and method calls in the controller dispatch chain:

At this stage, any parameter added in this way will be passed to the constructors for the Router, Dispatcher, and Action Controller objects; additionally, Zend_View_Helper will have a similar method for passing parameters to helper scripts. This methodology will help the API remain simple, while adding the flexibility and power desired for more advanced use cases.

2. References

3. Component Requirements, Constraints, and Acceptance Criteria

  • Allow Front Controller to work from a subdirectory easily
  • Allow for developer-defined routing easily
  • Provide tools and infrastructure for creating custom routing and dispatching
  • Provide methods for pushing data from the Front Controller through the controller chain
  • Decouple the MVC components from a web-only paradigm by allowing requests to come from any environment so long as they provide the controller and action
  • Allow basic usage via a one-liner, as well as more advanced usage

4. Dependencies on Other Framework Components

  • Zend_Request_Interface
  • Zend_Http_Request (optional)

5. Theory of Operation

Operation should allow for a simple static method that will create a default request object to push down through the controller chain, as well as advanced usage that accomodates custom routers, dispatchers, and request objects.

The following changes will need to be made.

Request Objects

The various members of the MVC working group identified the need for a Request object early. The rationale for it is to provide encapsulation of common functionality that would be needed for routing and dispatching, and to provide a replaceable object that can be used during testing on the CLI (allowing for flexible unit testing approaches). Additionally, by having generic request inheritance, MVC components could be used for non-web environments such as the CLI or GUI.

Request objects are primarily data containers, and typically shouldn't manipulate the data (other than setting based on input).

Zend_Request_Interface

Generic request interface. All request classes would implement it – Zend_Http_Request, Zend_Controller_Request, Zend_Cli_Request, etc., are all candidates.

Basic functionality is simply as an OOP accessor.

Zend_Http_Request

Proxies the HTTP request environment, providing accessors for $_GET, $_POST, $_COOKIE, $_SERVER, $_ENV, and PATH_INFO. Additionally, it contains methods accessors for the base URL and path.

Functionality will follow that of Mike Sheakoski's proposal. Additionally, it should provide functionality to get items out of the PATH_INFO by index and key (value would be from position(key) + 1)

Zend_Controller_Request_Abstract

Provides an interface of what needs to be provided to the controllers. Request objects used with the controller classes would need to implement this interface.

Defining such an interface allows developers to decouple their applications from the web, and could allow for use of the MVC components in CLI and GUI (e.g., PHP-GTK) environments.

Zend_Controller_Request_Http

Default Request object used by controller classes, this is an HTTP request. It extends the functionality of Zend_Http_Request by adding Controller specific methods (as defined in the Zend_Controller_Request_Abstract).

Router changes

Much of the functionality of the Router is now performed by the request. However, routing is beyond the scope of a Request object (which is a data container), and should have dedicated functionality. Such functionality may include regular expression matching of request parameters in order to determine the current route, using PATH_INFO indices to determine controller and action, etc. It would set the controller and action in the request and return the request object.

Dispatcher changes

Part of the discussion has centered around the idea of eliminating the Router and Dispatcher. As noted in the discussion of the Router and Request objects, above, each has a specific domain in which they work. In the case of the Dispatcher, keeping it in a separate class and following an interface allows some flexibility for advanced developers and/or special scenarios. One such scenario might be integrating legacy applications: a custom dispatcher could dispatch to legacy, procedural scripts instead of Action Controllers.

One common theme, however, is that the dispatcher would receive the Request object as its token instead of a Dispatcher Token. This allows the Dispatcher to have access to the entire request environment shared by all segments of the controller workflow. Zend_Controller_Dispatcher_Token and Zend_Controller_Dispatcher_Token_Interface will be removed entirely.

Zend_Controller_Dispatcher_Interface

Instead of receiving or returning dispatch tokens, Request objects will be used for the same purpose and more.

Zend_Controller_Dispatcher

The Dispatcher needs to be re-tooled to use the Request object. Additionally, instead of calling the Action Controller's run() method, it will perform actions similar to below:

Zend_Controller_Front

Current issues with the Front Controller include:

  • Action controllers have no knowledge of the request environment
  • Plugins, dispatchers, and routers do not share the same request information and thus are decoupled to the point of being crippled
  • No ability to pass extra custom parameters to the action controllers, dispatcher, or router

Changes necessary

  • Add methods
    • addParam()
  • Modify dispatch() method
    • Allow passing optional Zend_Controller_Request_Abstract object
    • Pass optional arguments to router, dispatcher
      • If optional arguments set via passToAction(), pass to dispatcher::passToAction()
    • Rework to utilize Zend_Controller_Request_Abstract object throughout dispatch process

Zend_Controller_Action

Current issues include:

  • No ability to do pre/post action items using the same controller instance
  • No knowledge of the request environment
  • No ability for a pre/post action to define the next action to call.

The proposed changes would remove the run() method from the dispatch loop to instead call the action directly. However, it would bookend this call with calls to preDispatch() and postDispatch() on the action controller, if defined. The postDispatch() method may perform extra tasks prior to passing on handling to the dispatch loop; by modifying the Request object and seeting its dispatched flag to false, it may also override what additional actions may be called in the dispatch loop. The preDispatch() method, by modifying the Request object and resetting its dispatched flag, can cause the dispatch loop to skip the current action and move on to the one now specified in the Request object.

An action may specify an additional action to perform by modifying the Request object and resetting its dispatched flag.

The constructor would receive the Request object at instantiation. Finally, the actions may receive extra parameters if the front controller and/or dispatcher has been passed information via addParam(). These are stored in the $_invokeArgs property, to which accessors will be provided.

In order to enforce the requirement that the constructor receive the Request object, the constructor will be made final, and will set the protected $_request property. A new init() method will be called as the final action during object instantiation and may be overridden by developers to provide custom constructor functionality.

run() would be retained for backwards compatability and to allow usage of the
Action Controller as a Page Controller. It will be made non-final, and will do
the following:

6. Milestones / Tasks

  1. Creation of Request interfaces and objects
  2. Retool Controllers (Front, Action), Dispatchers, and Routers to use Request objects; rework logic to accomodate.
  3. Testing; full unit test coverage
  4. Sync documentation with source changes

7. Class Index

  • Zend_Request_Interface
  • Zend_Http_Request implements Zend_Request_Interface
  • Zend_Controller_Request_Abstract
  • Zend_Controller_Request_Http extends Zend_Http_Request implements Zend_Controller_Request_Abstract
  • Zend_Controller_Router_Interface
  • Zend_Controller_Router
  • Zend_Controller_RewriteRouter
  • Zend_Controller_Dispatcher_Interface
  • Zend_Controller_Dispatcher
  • Zend_Controller_Action

8. Use Cases

UC-01
UC-02

9. Class Skeletons

]]></ac:plain-text-body></ac:macro>

]]></ac:plain-text-body></ac:macro>

Labels:
None
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Sep 22, 2006

    <p>Hey Matthew, looks good so far. I have a few questions:</p>

    <p><strong>Zend_Http_Request</strong></p>
    <ul>
    <li>getMethod() returns either GET or POST. What advantage does isPost() have over using something like <code>if ($request->getMethod() === 'GET')...</code>?</li>
    </ul>

    <p><strong>Zend_Request_Interface</strong></p>
    <ul>
    <li>What does has() do?</li>
    </ul>

    <p><strong>Front/Dispatcher/Action Stuff</strong><br />
    passToActionController() sounds like a nice idea since it passes directly to the action method. Would perhaps a better name be passToAction()? Also, my intent behind passToActionConstructor() was to be able to give the entire ActionController instance access to passed variables before any actions are called. This is the main reason I chose __construct(). How would I go about doing this since passToActionController() will only pass to the indexAction() method for example and not the class as a whole? Does init() accept passed variables instead?</p>

    1. Sep 22, 2006

      <ul class="alternate">
      <li>I think isPost() makes sense because of the oddity of HTTP that you always get GET, but only get POST when the method is POST.</li>
      </ul>

      <ul class="alternate">
      <li>has() is the equivilant of _<em>isset(). I think it is good to have both __get()/</em><em>set()/</em>_isset() and get()/set()/has() because you often have string keys and things isset($request->$key) are fine but a little odd. It is trivial to support I think the containers should support both.</li>
      </ul>

      1. Sep 22, 2006

        <p>Sounds good to me!</p>

    2. Sep 22, 2006

      <p>Chris has answered the request questions to my satisfaction already <ac:emoticon ac:name="smile" /></p>

      <p>I like the idea of shortinging passToActionController() simply passToAction().</p>

      <p>As for how it works, my intention all along was to pass it to the __construct() – but my class description doesn't show that at this time. I'm going to update the proposal to show that interaction.</p>

      1. Sep 22, 2006

        <p>I've updated the proposal to show how extra parameters will be handled by the Action Controllers.</p>

        1. Sep 22, 2006

          <p>This looks good. Currently in 0.1.5, Zend_Controller_Action::getParam() refers to params passed by the Request. In your implementation above they now reference params passed into the Constructor. Do you plan on keeping them separated or will $_params contain values from both Request and Constructor?</p>

          1. Oct 05, 2006

            <p>Actually, it's _getParam() and _getParams(), both protected. I'm updating the proposal to use _getInvokeArg/s() instead, as that would be clearer.</p>

  2. Sep 22, 2006

    <p>In the Dispatcher you have methods still returning nextAction. That seems leftover from when it was a Token. But now that the Request is passed throught it seem like the pre/postRun and the dispatched method could just as well set the new controller/action in the Request and return true. A forward() call could do this for you. </p>
    <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
    $params = null;
    if ($this->hasActionControllerParams()) {
    $params = $request->getParams();
    }
    $curAction = $this->formatActionName($request->getActionName());

    if ($controller->preRun($request)) {
    return true;
    }

    if ($controller->{$curAction}($request, $params)) {
    return true;
    }

    return $controller->postRun($request);
    ]]></ac:plain-text-body></ac:macro>
    <p>This also might allow more control flow because the return value controls short-circuiting, whereas the Request hold the forward value. Either the Request could set a flag when the controller/action are set or the Front Controller could check for a change and forward if it has. There are several possiblities here. </p>

    1. Oct 05, 2006

      <p>I'd been juggling ideas for handling this as I wrote the proposal, originally looking at having the dispatcher checking for changes to the request. However, what if the developer wants to loop through the same request multiple times?</p>

      <p>I like the idea of returning true if another action is to be called; this allows us to have the request simply act as a container, and then the action (or pre/postDispatch()) indicate if more activity is required.</p>

      <p>The one remaining issue: what if the action returns true, but postDispatch() is unimplemented or empty? I think we need some additional logic:</p>
      <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
      $nextAction = $controller->{$curAction}($request, $params);

      $nextPost = $controller->postDispatch($request);

      return $nextAction || $nextPost;
      ]]></ac:plain-text-body></ac:macro>

      1. Oct 05, 2006

        <p>What I did in my own personal implementation is have the ActionController return any user-defined value to the Dispatcher and then the Dispatcher returns that value to whatever called it. In most cases this would be the FrontController where if the returned value is a Request it continues looping, if NULL stops looping.</p>

        <p>The reason I did it this way is to allow for "portal-like" sites that are mashups of different controllers/actions/views:</p>

        <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
        class IndexController extends MJS_Controller_Action
        {
        public function indexAction()
        {
        $view = $this->view;
        $view->nav = $this->render('nav', 'index');
        $view->news = $this->render('news', 'list');
        $view->files = $this->render('documents', 'list');
        echo $view->render('Index/Index.php');
        }
        ]]></ac:plain-text-body></ac:macro>
        <p>The above code calls NavController::indexAction, NewsController::list, and DocumentsController::list which runs their respective actions and rendered views, capturing them using output buffering into a variable.</p>

        <p>The code I added to Zend_Controller_Action to do this is:</p>
        <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
        public function call($controllerName, $actionName = null, array $params = array())
        {
        $request = clone $this->_request;

        $request->setParams($params);
        $request->setParam('controller', $controllerName);
        $request->setParam('action', $actionName);

        return $this->_dispatcher->dispatch($request);
        }

        public function render($controllerName, $actionName = null, array $params = array())
        {
        ob_start();
        $this->call($controllerName, $actionName, $params);
        return ob_get_clean();
        }
        ]]></ac:plain-text-body></ac:macro>

        <p>From that you can see why I have the ActionController return any value to the Dispatcher.</p>

        1. Oct 06, 2006

          <p>I'm not exactly following that, but I am not sure that the response should be passed around by return value. A main View or real Response object would probably be better to handle that.</p>

      2. Oct 06, 2006

        <p>I agree that we may need some additional logic may be needed if the Plugin Manager does not do the check already. </p>

        <p>I don't think looping through the same request is a problem with the "return true" scheme. The reason is that what to forward and whether to forward have been separated, so you can just return true and not forward (i.e. specify a new Action) and it will reloop.</p>

  3. Nov 30, 2006

    <ac:macro ac:name="note"><ac:parameter ac:name="title">Zend Comment</ac:parameter><ac:rich-text-body>
    <p>The refactoring of MVC has now moved from Incubator to the Core library.</p>

    <p>Congratulations, MVC team!</p></ac:rich-text-body></ac:macro>

  4. Dec 05, 2006

    <p>This new MVC looks great!</p>

    <p>Only thing I want to suggest is: would it be possible to have an optional parameter in (for example) the Zend_Http_Request getPost() etc functions, so that you can define what will be returned when the key does not exist?</p>

    <p>For example the following:</p>

    <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
    public function getPost($key, $default = null)
    {
    return (isset($_POST[$key])) ? $_POST[$key] : $default;
    }
    ]]></ac:plain-text-body></ac:macro>

    <p>This will save having to wrap these functions when this is required.</p>

    1. Dec 05, 2006

      <p>Richard, great idea. I've added this capability in revision 2112.</p>

      1. Dec 05, 2006

        <p>I meant that you'd be able to specify the default value to return if the key doesn't exist.</p>

        <p>So if $_POST contains:</p>
        <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
        array(
        'action' => 'submit',
        'key_one' => '1',
        'key_two' => '2'
        );
        ]]></ac:plain-text-body></ac:macro>

        <p>Then </p>
        <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[$this->_getPost('action', 'cancel'); // returns 'submit'. If 'action' didn't exist it would return 'cancel'.]]></ac:plain-text-body></ac:macro>

        <p>ie:</p>
        <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[$this->_getPost('key_one', 0); // returns 1.]]></ac:plain-text-body></ac:macro>

        <p>but</p>
        <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[$this->_getPost('key_three', 0); // returns 0. (rather than null).]]></ac:plain-text-body></ac:macro>

        <p>What you added is very interesting too, though. Both features in combination would be incredibly useful!</p>

        1. Dec 05, 2006

          <p>Oops! I forgot where the request for returning the entire arrays came from, and wrongly attributed it here. I'll put a JIRA issue in for this and address it this week.</p>

          1. Dec 05, 2006

            <p>Issue is at <a class="external-link" href="http://framework.zend.com/issues/browse/ZF-620">http://framework.zend.com/issues/browse/ZF-620</a></p>

            1. Dec 05, 2006

              <p>Righto, thankyou!</p>

            2. Dec 05, 2006

              <p>Done in revision 2116.</p>

  5. Dec 20, 2006

    <p>The new MVC components have the capability not only to handle controllers and actions out of the box but also modules. Very cool stuff! <ac:emoticon ac:name="smile" /> But unfortunately </p>
    <ac:macro ac:name="code"><ac:parameter ac:name="title">Zend/Controller/Action.php</ac:parameter><ac:parameter ac:name="borderStyle">solid</ac:parameter><ac:plain-text-body><![CDATA[Zend_Controller_Action::_forward()
    ]]></ac:plain-text-body></ac:macro>
    <p> is final and does not provide a module parameter you have to use a evil hack to forward a request to a different module. Right now I am using a postDispatch Plugin to look if there was a magic "module" parameter set in $pamameter and if so route it to the right controller directory accordingly. I think this kind of functionality belongs into the basic Zend_Controller_Action::_forward() implementation since the module concept is also part of the base MVC components.</p>