A proof-of-concept has been developed and is available at:
The goals of the rewrite are to make a component that is:
- Easy to extend
- Easy to create and use custom implementations
In evaluating architectures, two principal ideas rose as potential solutions:
- Finite State Machine (FSM)
- Event-driven model
The reasons an FSM were considered were several. First, the front controller defines several distinct states already: routing, dispatching, and emitting the response; errors are also a distinct state, though currently muddled into the rest of the architecture. (You could potentially add "bootstrapping" to that list.) However, in current ZF iterations, there's no easy way to transition between or skip states; e.g., if an exception is detected, an entire iteration of the dispatch loop is executed; if a redirect is made, you have to either complete the remainder of the dispatch loop or call "exit" (a horrible hack).
A Finite State Machine largely solves these problems. If you need to go to another state, you simply goto it. That said, there are some limitations to goto: it only works with labels defined in the current scope; you cannot call goto from an invoked method, for instance. One way to mitigate this is to keep track of state in an object.
At this point, an event driven model makes sense. In this case, a token, the event, is passed to all invoked methods. This allows those methods to alter the event state such that when the process returns to the invoking method, the state may be checked and control dispatched to the appropriate state accordingly.
To make the concept more powerful, if each state allows for one or more topics to which listeners may subscribe, complex chains of actions may be created easily. Using Phly_PubSub (to be proposed to ZF as well), this also allows us to check the event state after each subscriber completes to determine if we need to transition to a new state – allowing the subscribers to help determine execution flow.
Each subscriber is passed one argument and one argument only: the Event. As such, the event now becomes a repository/service locator. In the proposed MVC implementation, it holds the request and response objects, the pubsub provider (so additional subscribers may be registered at any time), and the state; it can potentially hold anything, however, as it extends ArrayObject.
Basic operation is something like this:
The actual workings are slightly more complex, but the ideas hold.
The ramifications of this architecture are intriguing:
- There's no need to use any ZF classes with the front controller (other than the event, event manager (pubsub), and front controller); you can potentially create your own structure and callbacks to use.
- If interfaces are provided, they simply provide a hook into the default use case we choose to provide.
- Developers could potentially override the FrontController and provide their own application states, providing for custom workflows.
The proposed MVC does aim to provide a default work flow for the front controller. This work flow will include:
- Request object
This object will simply provide access to the current environment, much as Zend_Controller_Request does already. The primary differences will be that injection into the various "superglobal" accessors will be built in.
- Response object
This object will be a container for response-related items, including headers, values related to rendering, and potentially cookies. The primary interface method will be sendOutput(), which will be used to serialize the various items into output. Serialization will primarily be done via Renderers.
- Renderer object
A Renderer will take response values and determine what to do with them. The default use case will be to use Zend_View + Zend_Layout to produce content.
- Router object
Pulls the response from the event, and then decomposes that into key/value pairs to inject in the request.
- Dispatcher object
Determines how to dispatch the request. The default use case will be to determine the module, controller, and action from the request object, and then simply instantiate and invoke the appropriate class; potentially, however, you could use any logic you like, including simply providing closures.
- Action controllers
These will work basically the same as they do currently, with a few exceptions. First, the only necessary method will be __invoke(), and it should accept an Event object. From there, how it matches the action to functionality is up to the developer.
The base ActionController class will provide some standard functionality surrounding this, and will attempt to invoke *Action() methods, just as Zend_Controller_Action does currently. Additionally, it will add logic to __call() to automatically delegate to action delegates and action helpers.
- ErrorHandler object
Receives errors and updates the event state.
- Front controller plugins would no longer need to rely on an abstract; you * would simply subscribe individual plugin classes/instances or closures with the appropriate topic in the pubsub provider,
- The action helper broker can be injected into the Event, and the action controller can then pull it from there. This helps eliminate the singleton factory it currently implements.
- The view object becomes subservient to the renderer, which is subservient to the response. This will require potentially more scaffolding during bootstrap and/or when determining what renderer to utilize. standard mvc.response.pre plugins could alleviate the situation.
- Even though a default imlementation will be provided, the possibility exists for multiple implementations – and for community-led implementations for specific purposes.
- Potentially huge performance gains, particularly if you attach pubsub callbacks on-demand based on the application flow.
In an action, _getParam() doesn't provide any filtering of superglobals, which can lead to potential security issues if you do not filter the data within your action controllers.
One suggestion is to allow attaching filters/validators to the request object, and only allowing direct access to superglobal values via getRaw*() methods – essentially a poka yoke.
Some proposed changes:
Reinstate the noRouteAction() (or something similar) as an empty method (throwing an exception) in the action controller. __call() would then detect method calls ending in 'Action' and call this method.
By itself, this does not represent a BC break; _call() would simply proxy to this method, which would throw the same exception as currently thrown in call(). Overriding the method would be similar to overriding _call() in current versions.
_call() would be modified to determine if a method call is a helper name, and, if so, invoke it. _get() would be added, and would do similarly, only it would return the helper directly. This would make calls to helpers much simpler, and make them mimic the behavior of view helpers more directly - leading to better consistency in the framework in general and MVC layer in particular.
As an example,
would simply become:
As an example showing property access:
This would be a slight BC break - but only for developers overriding _call(), and then the BC issues would be limited in scope. $_helper would continue to be registered, so all previous calls to helpers would work. When overriding _call(), you simply wouldn't be able to call helpers as controller methods, since the logic to proxy to the helper broker would not be present.