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_Controller_Action_Helper Component Proposal

Proposed Component Name Zend_Controller_Action_Helper
Developer Notes http://framework.zend.com/wiki/display/ZFDEV/Zend_Controller_Action_Helper
Proposers Ralph Schindler
Revision 1.1 - 1 March 2007: Updated from community comments. (wiki revision: 8)

Table of Contents

1. Overview

The goal of Zend_Controller_Action_Helper is to supply Action Controllers with runtime/on-demand helper functionality. Currently, the only way to inject functionality into the abstract Action Controller is by extending it with the desired common functionality as methods of the abstract class. Action Helpers aim to minimize the necessity to extend the abstract action controller in order to inject common action controller functionality.

Why not the current Plugin system? The current plugin system is a product of the Front Controller and thusly hooks into the various stages of dispatching from routing,looping,then the superficial dispatching. (I say superficial since it in no way interacts with the Action Controller from the point at which execution is handed off). Currently, Plugins are a one-way street. Beyond being registered, a developer cannot interact with them easily from the Action Controller layer. It could be said that there is ZERO overlap in these two systems. In short, think of it like this: Existing "Plugins" are plugins to the Front Controller as Action Helpers are plugins to the Action Controller.

Action Helpers exist independently of the chosen method of Dispatching (Front Controller or Page Controller), thusly a developers can be certain that all action helpers will work the same way regardless of the method of dispatchment.

Action Helpers (like Zend_View_Helpers) are minimally loaded and called on demand; maximumly can be instantiated at request time (bootstrap) or action controller creation time (init()).

Most immediately, and included in this proposal, candidates for Action Helpers are redirect functionality (which currently exists as Action Controller core functionality.. argument can be made as to wether or not it belongs there), and flash messaging (or FlashMessenger). These are common tasks that the overwhelming majority of developers have used in one form or another.. and have roots in existing frameworks.

IMPACT ON EXISTING FRAMEWORK

Minimal at best.. I counted 5 lines of changes to Action.php, the rest is included and run on demand as noted above. The existing functionality of _redirect can be replaced (to depreciate if desired) with the same code that would be expected from developers from within their own Action Controllers... another few lines at best.

(Future) Many different Action Helpers could potentially become part of the Zend Framework core with zero modifications to core files once the Hooks for Action Helpers are put in place (much the same way Zend_View_Helpers can be introduced into the framework via the proposal process.

2. References

3. Component Requirements, Constraints, and Acceptance Criteria

none

4. Dependencies on Other Framework Components

  • Zend_Controller_Action

5. Theory of Operation

see usage case 1

6. Milestones / Tasks

to be determined

7. Class Index

  • Zend_Controller_Action_Exception
  • Zend_Controller_Action_HelperBroker
  • Zend_Controller_Action_Helper_Abstract
  • Zend_Controller_Action_Helper_Redirector
  • Zend_Controller_Action_Helper_FlashMessenger

8. Use Cases

9. Class Skeletons

HelperBroker.php

Helper/Abstract.php

Helper/Redirector.php

Helper/FlashMessenger.php

]]></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. Mar 03, 2007

    <p>Ralph – publicly commenting here what I wrote to you earlier. </p>

    <p>Basically, why not use the existing plugin broker to do this? </p>

    <p>So, in Zend_Controller_Dispatcher_Standard::dispatch(), there'd be a line like this:</p>
    <ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
    $controller->setPluginBroker($this->getFrontController()->getPluginBroker());
    ]]></ac:plain-text-body></ac:macro>
    <p>And in Zend_Controller_Action:</p>
    <ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
    public function setPluginBroker(Zend_Controller_Plugin_Interface $broker)

    Unknown macro: { $this->_plugins = $broker; }

    ]]></ac:plain-text-body></ac:macro>
    <p>For now, I could see adding the following plugin hooks to the plugin interface:</p>
    <ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
    // No args defined yet; need to determine what would be necessary
    public function redirect();

    // forward to another method using $args array
    public function forward(Zend_Controller_Request_Abstract $request, array $args);

    // view rendering; response is optional, as it should be set in the
    // plugin object already
    public function render($spec, Zend_Controller_Response_Abstract $response = null);
    ]]></ac:plain-text-body></ac:macro>
    <p>Of course, not all plugin hooks would need to be placed in the interface; any given plugin class could implement additional methods.</p>

    <p>To allow the plugin broker to understand hooks not in the interface, we could add the following to the plugin broker:</p>
    <ul>
    <li>registerHook($name);</li>
    <li>unregisterHook($name);</li>
    <li>getRegisteredHooks($name);</li>
    </ul>

    <p>Then, __call() could check to see if the method called matches registered hooks, and, if so, attempt to call the hook method in each plugin (checking for existence of the hook method first). Some caching ability would be needed, so that hooks that are used multiple times don't require the is_callable() calls each time they are invoked.</p>

    <p>This would:</p>
    <ul>
    <li>re-use existing functionality</li>
    <li>define a single location for extension of the controller classes (the plugin system)</li>
    <li>allow plugins for both Action and Page Controller paradigms</li>
    <li>work basically the same as the action helpers you're proposing</li>
    </ul>

    <p>Thoughts?</p>

  2. Mar 03, 2007

    <p>Matthew, I have updated the proposal above to get my thoughts more collected on what this proposal strives to do. In general, I understand the pressure of being feature complete and moving to v1.0, and understand not wanting to add this right now.. But on the other hand, take a look at the overall impact it would have on the existing framework (detailed above) VS. the overall gain for developers.</p>

    <p>Above I noted my general thoughts on what this is vs. what plugins offer and are currently designed for....</p>

    <p>I am not opposed to incorporating them into plugins, but that would mean we have to redefine what a plugin is ultimately.</p>

    <p>Going through your comments:</p>
    <ul>
    <li>I dont think adding those stock methods (redirect,forward,render) really belong in the domain of the plugin interface... perhaps you mean that there would be a redirector plugin, forwarder plugin, and renderer plugin?</li>
    <li>Do you mean that the __call would be trapped by the broker? I think that is the appropriate place, and in my current working code, __call is actually trapped by the broker.. it cant be trapped by the Action Controller b/c that would mean that name overlap between methods and plugins could occur.</li>
    <li>in "allow plugins for both Action and Page Controller paradigms" do yo mean Front Controller and Page Controller paradigms?</li>
    </ul>

    <p>Honestly, I think this is totally doable (if its wanted) within the next week. The code (For this iteration is done and tested and works well).. I think its architecture is solid (it borrows alot from an existing example and also borrows style from the rest of Zend_Controller).</p>

    <p>Part of me thinks that since we are 2 weeks prior to feature freeze, and since it has such a small footprint in the existing framework, we could get it done prior to 1.0.. but if not, thats fine too.. the only thing I ask is that you think of the benefits.</p>

    <p>-Ralph</p>

  3. Mar 04, 2007

    <p>Matthew W,<br />
    I have slept on it, and perhaps 'plugins' could be just as effective of a place to deploy this type of functionality.. A few things would need to happen in order to do that:</p>

    <ul>
    <li>the goal of plugins would have to extend to more than just plugging into the Front Controller, but present itself as a way to plug into all aspects of Zend_Controller, thus allowing us to plug into Action Controllers. This would allow us to create plugins for the specific use of Action Controller Plugins.</li>
    <li>Action Controllers need to be made 'Plugin Aware', this would be done by creating an internal property called $_plugin that (at the instance creation of each action) retrieves a Plugin_Broker.</li>
    <li>Hooks (methods) must be created in the Plugin interfacxe to accomodate the post init() action specific preDispatch() and postDispatch() runtime hooks.</li>
    <li>The on-demand features and prefix/path registration for custom components should be added the to plugin broker.</li>
    </ul>

    <p>All in all, there are few actual additions really going on here, most of it is just redefining the role of Plugins. This change I think also would bring plugins closer to the fore-front and a much more attractive feature of Zend_Controller.. all without overloading the user with another 'feature'.</p>

    <p>What do you think?<br />
    -ralph</p>

  4. Mar 05, 2007

    <h3>Defining The Goals of the Zend_Controller_Plugin System</h3>

    <p>1. Allow two way communication with registered and on-demand plugins.</p>
    <ul>
    <li>accomplised by making Abstract Action Controller 'plugin broker aware'</li>
    <li>getPlugin($name) would return the plugin object
    <ul>
    <li>plugin object is broker aware</li>
    <li>plugin object is action controller aware</li>
    </ul>
    </li>
    </ul>

    <p>2. Allow for on demand loading by plugin name.</p>
    <ul>
    <li>The same as how Zend_View helpers work, with prefix and path supplied.</li>
    </ul>

    <p>3. Allow broker to accept method callbacks (hooks) that will be run<br />
    at specific intervals in the lifecycle of Zend_Controller</p>
    <ul>
    <li>Valid Hooks (in tree layer form):
    <ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[

      • RouteStartup/route_startup |notify initiated from front controller
      • DispatchLoopStartup |notify initiated from front controller
      • ActionInit |notify initiated from action controller
      • PreDispatch |notify initiated from action controller
      • PostDispatch |notify initiated from action controller
      • DispatchLoopShutdown |notify initiated from front controller
      • RouteShutdown |notify initiated from front controller
        ]]></ac:plain-text-body></ac:macro></li>
        </ul>

    <p>4. Allow for Plugin Broker to accept arbitrary object callbacks registered<br />
    at any of the hookpoints defined above.</p>
    <ul>
    <li>ex: $broker->registerHook($object, $method, $hookTime);</li>
    </ul>

    <p>5. Allow for magic request (__call) forwarding when Plugin Broker gets request</p>
    <ul>
    <li>By Plugin name, forwared to _direct plugin method</li>
    <li>By Registered Method Name (4), forwarded to registered landing pad</li>
    </ul>

    <h3>Questions:</h3>

    <p>Why should sequence be important?<br />
    What really is this runOne flag when talking about an explict registration system.<br />
    Fully Explain why scanning is important.</p>

    1. Mar 06, 2007

      <ol>
      <li>I would make the following methods available, in both the action controller and front controller:
      <ul>
      <li>setPluginBroker(Zend_Controller_Plugin_Broker $broker); register a plugin broker</li>
      <li>getPluginBroker(); retrieve the plugin broker</li>
      <li>getPlugins(); returns all plugins</li>
      <li>getPlugin($class); returns a plugin of class $class; if more than one<br />
      registered, returns an array</li>
      <li>getHooks(); returns list of all hooks</li>
      <li>getHook($hook); returns array of plugins that implement $hook</li>
      </ul>
      </li>
      <li>I like the idea of on demand loading, and actually implemented this in a front controller extension I was working on recently. As you'll see from the sample code below, it simply took a directory and prefix, and then loaded the plugins found in that directory. This is different than the Zend_View-style implementation, as we cannot necessarily assume that a plugin class implements only a single hook.
      <ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
      /**

      • Provide a directory of plugins to load
        *
      • Given a $directory, loads all PHP files in it as plugins. The filename
      • is used to determine the plugin class name; if a $classPrefix is
      • provided, it will be used to prefix the plugin class.
        *
      • Plugins are registered by instantiating and then passing to
      • Unknown macro: {@link registerPlugin()}

        .

      • @param string $directory
      • @param string $classPrefix
      • @return Zend_Controller_Front
        */
        public function addPluginDirectory($directory, $classPrefix = '')
        {
        if (!file_exists($directory))
        Unknown macro: { require_once 'Zend/Controller/Exception.php'; throw new Zend_Controller_Exception('Invalid plugin directory specified'); }

      $list = new DirectoryIterator($directory);
      foreach ($list as $file) {
      if ($file->isDir() || $file->isDot() || !$file->isReadable())

      Unknown macro: { continue; }

      $name = $file->getFilename();
      if ('.php' != substr($name, -4))

      $class = substr($name, 0, strlen($name) - 4);
      if (!empty($classPrefix))

      Unknown macro: { $class = $classPrefix . $class; }

      $fileName = $file->getPath() . DIRECTORY_SEPARATOR . $name;
      include_once $fileName;
      if (!class_exists($class))

      Unknown macro: { require_once 'Zend/Controller/Exception.php'; throw new Zend_Controller_Exception('Invalid plugin class "' . $class . '"; not found in ' . $fileName); }

      $this->registerPlugin(new $class());
      }

      return $this;
      }
      ]]></ac:plain-text-body></ac:macro></li>
      <li>To clarify, we'd have addHook() and removeHook() methods, but those required for controller flow would always be protected. Additionally, as I noted to you yesterday, pre/postDispatch() exist in both the front controller and action controllers for a specific reason: to allow global pre/postDispatch() actions (front controller), as well as application specific actions (action controller). I'm not sure we need an action controller level hook for this, as it is part of its internal dispatch() process.</li>
      <li>I hadn't considered adding just a callback; interesting idea. I think it should register a callback with an existing hook, though (or, if the hook does not exist, it would create a new hook). The signature would then look like registerHookCallback($callback, $hook);</li>
      <li>I see __call() happening substantially different from how you detail it:
      <ul>
      <li>when a plugin is registered, the broker scans its methods against the available hooks; if a method matches a hook, it is added to a callback stack for that hook.</li>
      <li>__call() then determines if the method called matches a registered hook; if so, it then loops through each callback in that hook's stack.</li>
      <li>Finally, for the official hooks, we'd have a single method each; this would reduce the overhead of __call().</li>
      </ul>
      </li>
      </ol>

      <p>As for your questions:</p>
      <ul>
      <li>Sequence is important as the developer may want to depend on things happening in a particular order. For example, a dispatchLoopShutdown() hook may call plugins to put content in a sitewide template and to tidy the result. If sequence was discounted, the tidy call may happen prior to the final content being generated. Note that I'm not talking about the sequence of hooks, but of the sequence of callbacks in an individual hook. The plugin broker is agnostic of the order in which hooks are called; that's the front and action controller's jobs.</li>
      <li>The runOne flag is my proposal. There are some hooks that may not be suited to a stack of plugins: redirect and forward hooks, for instance, could raise potential issues, as could a generic render hook. The idea I had was addHook($name, $runOne = false); by setting $runOne to true, you'd be hinting to the broker to run only the last registered callback for that hook.</li>
      <li>Scanning would occur whenever (a) a new plugin is registered, or (b) a new hook is registered when plugins are already registered with the broker. This scanning would help keep the usage of the broker transparent, simplify the interface (developers would not need to know what hooks a plugin class implemented), and keep it flexible (hooks and plugins could be added in any order, and at any time). Regarding (a) and (b):
      <ul>
      <li>When a new plugin is registered, its public methods would be retrieved via get_class_methods() and stored in the broker. These would then be compared against registered hooks; if any match, a callback to that method is added to that hook's callback stack.</li>
      <li>When a new hook is registered when plugins are already registered with the broker, the broker would go through the list of plugins and their cached methods, adding any matching plugin/method callbacks to the new hook.</li>
      </ul>
      </li>
      </ul>

      1. Jul 05, 2010

        <p>substr($name, 0, strlen($name) - 4) can be written as substr($name, 0, -4)</p>

  5. Mar 06, 2007

    <p>I think both our paradigms of what Plugins are, differ.  That's why I am trying to refine the goals overall.  I try not to think of plugins as a stack, they are a repository of objects.  The only stacks I imaging are the registered callbacks within the difference hookpoints.  These callbacks would be registered (Currently they are used by filling in the method.)</p>

    <p> So you can see where my point of view is coming from, my attempt is to create a class that will serve as the FlashMessenger.  Ideally, FlashMessenger would exist (Be available through) the Action controller, and is accessible from the action controller on-demand.  The only hookpoint FlashMessenger would be interested in knowing of, would be when an action is postDispatched to do some internal namespace cleanup incase a _forward has been issued.</p>

    <p> The problem with not having the hooks inside the Action Controller means that they are no longer Action Controller hooks, they are Front Controller hooks.. also, in the current iteration, init() would run before you get a chance to register the plugin broker, essentially making plugins untouchable until the actual controller/action is dispatched. (Same reason why you pass the request and response in the constructor)</p>

    <p> To address the goals by number:</p>
    <ol>
    <li>Yes, those methods look good, they foster an all around awareness in both the Front Controller and Action Controller.. As I mentioned above, the timing of the setting of the plugin broker and init() call in the action controller constructor would need to be worked out.</li>
    <li>That doesnt satisfy the on-demand nature of plugins.  In your current paradigm/mindset, you are equating plugins only with objects concerned with sequentially dispatched output modifiers.  This paradigm is what I am trying to expand into classes that aren't necessarily concerned with hooking into the lifecycle of controller sequenced calls.. In that paradigm of plugins, redirector and flashmessenger belong in plugins.  The reason I started thinking it might be best to have Action Helpers become plugins is b/c essentially, we are plugging classes into the controller component with the intention of having them be controller aware.</li>
    <li>The actual notification of preDispatch() postDispatch() is only in the front controller, which consequently is before the Action Controller init() method and action controller __construct().  This means plugins will not have a reference to the Action Controller at preDispatch time.  Hence, they are not Action Controller aware.</li>
    <li>This concept is nothing important, it only builds on the idea of explicit registration. (see note below about scanning)</li>
    <li>I think this is another area where our paradigms diverge.  I can only assume when you think about this __call() in practice, you are thinking about plugins that implements a common render() command, or something to that effect.  I agree that you cant assume all plugins will implement only one public method which is why i proposed that getPlugin is a method in Action Controller.. to retrieve the entire plugin object (which is completely controller aware).  Also, for simplicity's sake, __call allowed for the forwarding of a single method for plugins where one method makes the most sense.  Something for all.</li>
    </ol>

    <p>On to the questions</p>
    <ul>
    <li>I agree, sequencing is important, which is why in the current iteration, hook stacks are executed in the order the plugins were registered.  First In - First Run.  Perhaps an optional argument for a Z layer option when registering.  This is a common technique in both Flash (Actionscript) and DHTML</li>
    <li>I actually dont see redirecting working any different than it does currently, the only difference is that the code for redirecting lives in the redirector plugin, not in the abstract action.  It does nto need to hook into any of the lifecycle currently, I am not sure why it would need to hook into any aspect of the lifecycle as a plugin?  Same for Forwarding / forwarder.</li>
    <li>I am not a fan of scanning as much of a fan of explicit registration.  This is why currently (behind the scences) plugins get a reference to the broker, so that at registration time, when the broker runs the plugins initHooks() method, the plugin can register hooks with the broker.  That way the plugin developer can explicitly set which methods will get called by the broker when specific lifecycle hookpoints are reached.  This was demonstrated with the ZDev_Plugin_Test i showed you, located: <a class="external-link" href="http://svn.ralphschindler.com/repo/ZendFrameworkBranches/Zend_Controller_Plugin_Enhancements-r3734/doc/Test.php">http://svn.ralphschindler.com/repo/ZendFrameworkBranches/Zend_Controller_Plugin_Enhancements-r3734/doc/Test.php</a></li>
    </ul>

    <h4>Summary</h4>

    <p>what i propose is actually not overly complicated changes.. in fact, we dont need the dynamic hook registration at all.  I simply want a way to make action controller aware plugins available to action controllers through $_plugin property.  This also means that the notification points are moved to a more sensible location... (plugin broker registered before init() is called)</p>

    <p> In the end, this would allow simple to complex action controller functionality to be injected without having to extend the abstract action controller.</p>

  6. Mar 07, 2007

    <p>Ralph – I've had a change of heart and decided that helpers and plugins should probably remain separate. Plugins have to do with event notifications, whereas helpers are to allow on-demand loading of additional features. I support your original proposal as it was, with perhaps some minor changes to allow easy calling of helpers by name (utilizing __call() in the helper broker).</p>

  7. Mar 08, 2007

    <p>Ok, then this brings us back to the original branch:</p>

    <p><a class="external-link" href="http://svn.ralphschindler.com/repo/ZendFrameworkBranches/Zend_Controller_Action_Helper-r3715/">http://svn.ralphschindler.com/repo/ZendFrameworkBranches/Zend_Controller_Action_Helper-r3715/</a></p>

    1. Mar 08, 2007

      <p>I see tremendous long-term value in supporting "on-demand" plugins for action controllers. I would vote to allow this plugin system to evolve with its tightly focused purpose, before considering more complex ways to abstract and factor plugin brokers in general. I vote to squeeze this into ZF 0.9, if possible and practical.</p>

      <p>Regardless of the plugin examples' merits, the architecture itself fits nicely with the flexibility offered by the ZF, and particularly the controller-related components. I imagine developers conveniently sharing small packages of functionality via action helpers. I can even imagine things like an implementation of output buffering for action controllers using the plugin system.</p>

      <p>I have one concern about the execution of multiple actions, each responsible for building some portion of a web page. If action helpers are used in these actions (possibly the same action helper class in more than one action controller), then I hope the implementation accomodates this, without relying on side-effects or userland managed state stored in static variables. In other words, I hope to nest actions and views, both using their respective helpers, without worrying about unexepcted side-effects contaminating the behavior of action helpers used in unrelated action controllers.</p>

      <p>Key advantages of action helpers (for me):<br />
      + merely loading an action helper connects the helper's pre/post dispatch methods<br />
      + convenience: merely loading the helper provides easy access to its public methods from within the action controller<br />
      + helpers can be as easy as "drop in place and use" for my apps</p>

      <p>Q: Why not keep the helpers as standalone classes?<br />
      A: Action controller would then have to load the standalone helper, register it with pre/post dispatch hooks (defined/added to the action controller abstract), then initialize it, passing in the action controller itself in order to make various things possible from with the standalone class, such as found currently in _redirect (which needs to examine information in the request object).</p>

      <ol>
      <li>front controller routeStartup()</li>
      <li>front controller runs the router to map the request object to a module, controller, and action</li>
      <li>front controller routeShutdown()</li>
      <li>front controller dispatchLoopStartup()</li>
      <li>front controller enters dispatch loop
      <ol>
      <li>dispatcher instantiates controller</li>
      <li>controller initializes via init()</li>
      <li>controller loads and initializes action helpers</li>
      <li>controller preDispatch, also calls action helpers' preDispatch</li>
      <li>controller $action</li>
      <li>controller postDispatch, also calls action helpers' postDispatch</li>
      <li>continue dispatch loop, unless the request is marked as "dispatched"</li>
      </ol>
      </li>
      <li>front controller dispatchLoopShutdown()</li>
      </ol>

      1. Mar 09, 2007

        <p>Paragraph 3 is overly cryptic. Specifically, I am imagining a common use case, where a user visits a portal website to view their customized home page. The user has selected to enable the "summary of recent posts", "friends online", and "feature photo of the day" on their personalized home page. Each of these three sections on their page are supported by unrelated action controllers in different modules, possibly written by different developers (for larger websites).</p>

        <p>If my personalized home page has 3 <div>'s containing "my news", "my friends online", "recent posts", etc., then the computation of each should not be adversely affected by the others through side-effects in action controller helpers. For larger web applications, I would expect each of these 3 content areas involve multiple modules, possibly written and maintained by different developers. If the action helpers are serially reusable without side-effects, and only affect the private, local state of the action controller they are helping, then I can use or nest action controllers without adverse or unexpected side-effects breaking code in another developer's module that happens to use the same action controller helper.</p>

        <p>For example, if the forum module allows anonymous viewing of some topics, but not others, and uses the "authenticate action controller helper", <strong>and</strong> it updates the count of how many authentication attempts have been made (side-effect) every time it is called with the same input in the same request, <strong>then</strong> when the same helper is used by "my friends online" I could end up with an inaccurate count of the total number of authentication attempts. Yes, this example is contrived, but hopefully it helps explain what I mean by "undesireable side-effects".</p>

  8. Mar 09, 2007

    <ac:macro ac:name="note"><ac:parameter ac:name="title">"Official Zend Comment"</ac:parameter><ac:rich-text-body>
    <p>This proposed enhancement is approved for development in the incubator.<br />
    Optionally you may work in the core directory, in a svn branch off of the trunk.</p>

    <p>The helper design is fine, this allows the maximum flexibility so that the developer can effectively design custom hooks. This is more flexible than introducing lots of immutable hooks in the action controller.</p>

    <p>The request I would have is that in the documentation for this feature, you make it very clear that it is optional to define helpers at all. We don't want new users to think that this is a mandatory piece of work that they need to think about as they design their simple action controllers.</p></ac:rich-text-body></ac:macro>