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

Proposed Component Name Zend_Db_Table_Plugin
Developer Notes http://framework.zend.com/wiki/display/ZFDEV/Zend_Db_Table_Plugin
Proposers Simon Mundy, Jack Sleight
Matthew Weier O'Phinney (Zend Liaison)
Revision 0.2 - 23 February 2008
0.1 - 9 December 2007 (wiki revision: 17)

Table of Contents

1. Overview

Zend_Db_Table_Plugin provides a plugin system for Zend_Db_Table, in order to easily run common business logic across models. Plugins are registered either with the Plugin Broker (to have them attached to all Zend_Db_Table instances) or set individually from within each model class.

2. References

3. Component Requirements, Constraints, and Acceptance Criteria

  • This component will allow fine-grain control over behaviours to be added to Zend_Db_Table, Zend_Db_Table_Rowset and Zend_Db_Table_Row read/write operations.
  • This component will allow for developer-defined namespaced plugins similar to existing Plugin loader behaviour

4. Dependencies on Other Framework Components

  • Zend_Exception
  • Zend_Db_Table

5. Theory of Operation

Plugins are designed to allow business logic to be applied to Zend_Db_Table and associated rows/rowsets in an extensible, flexible manner. They act in a manner similar to MVC plugins, allowing access to Table, Rowset and Row objects both before and after read/write operations. Plugins are processed in FIFO order and it is possible to specify plugin loading order during instantiation.

The first method to add plugins is via the Plugin Broker, whereby each plugin attached is automatically added to each Zend_Db_Table upon instantiation. It is recommended to perform this operation prior to dispatch, ideally within a bootstrap - the Plugin Broker can receive a $config object or array to automate this procedure.

The second method is to add them on a per-table basis, either through configuration options or by the 'addPlugin' method. The '_plugin' property will allow array-based configuration (identical to that of the Plugin Broker) to create plugins and optionally pass options per-plugin.

Plugins can also be serialized with their respective Table/Rowset/Rows.

All 'pre' event methods return a null by default. This allows an operation to continue as expected, however this can be overidden by returning a non-null value. This may be useful for situations where the default behaviour is determined to be undesirable - for example, cancelling a 'delete' operation or returning a cached value instead of a 'live' query.

6. Milestones / Tasks

  • Milestone 1: DONE Write proposal
  • Milestone 2: IN PROGRESS Gather feedback and revise design as necessary
  • Milestone 3: Review by the Zend team
  • Milestone 4: Develop full implementation and unit tests
  • Milestone 5: Initial documentation

7. Class Index

  • Zend_Db_Table_Plugin_Interface
  • Zend_Db_Table_Plugin_Broker
  • Zend_Db_Table_Plugin_Abstract
  • Zend_Db_Table_Plugin_Exception

8. Use Cases

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. Dec 14, 2007

    <p>Great start! I know this proposal is in it's infancy but I would like to give a few suggestions to help jumpstart this proposal.</p>

    <p>1. IMO, for all row-based methods, it's redundant to include $table as one of the parameters since the $row already has a public method to fetch the table: Zend_Db_Table_Row_Abstract::getTable();</p>

    <p>2. Zend_Db_Table_Plugin_Interface::getRowColumn doens't need the 3rd param "$value":<br />
    public function getRowColumn(Zend_Db_Table_Row_Abstract $row, $columnName);</p>

    <p>3. In your My_Plugin_Date example above I already see some issues with redundancy. Many of these types of callbacks require the same functionality "onSave" (not necessarily insert or delete). What are your thoughts on just having a preSave() and postSave() then adding the following function to Zend_Db_Table_Row_Abstract?</p>
    <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
    class Zend_Db_Table_Row_Abstract
    {
    // ...
    public function isNew()

    Unknown macro: { return empty($this->_cleanData); }

    // ...
    }

    class My_Plugin_Date extends Zend_Db_Table_Plugin_Abstract
    {
    public function preSave(Zend_Db_Table_Row_Abstract $row)
    {
    $date = Zend_Date::now();
    if (true === $row->isNew())

    Unknown macro: { $row->created = $date->getIso(); }

    $row->modified = $date->getIso();
    }
    }
    ]]></ac:plain-text-body></ac:macro>

    <p>4. What about table-based settings? For instance with your My_Plugin_Date 'created' and 'modified' are hardcoded. Allowing the user to specify which columns represent these two cases would extend the functionality further:</p>

    <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
    class Article extends Zend_Db_Table_Abstract
    {
    protected $_plugins = array(
    'My_Plugin_Date' => array(
    'onInsert' => array('created', 'created_on', 'inserted'), // multiple fields
    'onUpdate' => 'modified' // or just a single field
    )
    );
    }
    ]]></ac:plain-text-body></ac:macro>

    1. Dec 14, 2007

      <p>Hi Brandon, <br />
      Thanks for the suggestions. As you mentioned, the proposal is still in very early stages, and Simon and I have been discussed many things that are yet to be posted here.</p>

      <p>1. I agree, no need to overcomplicate the API.</p>

      <p>2. Good point, my mistake <ac:emoticon ac:name="smile" /></p>

      <p>3. I like this idea, however I think that for the sake of consistency, the API should closely match that of Zend_Db_Table_Row_Abstract, and that does not currently have pre/postSave methods. If we added them to the plugin interface (which I would like to), it would make sense to add them to Zend_Db_Table_Row_Abstract as well, but that's not my call. I'll ask Simon.</p>

      <p>4. So that array would be like an options array, which is parsed to a setOptions() method in the plugin? I think that would work, I'll talk it over with Simon and see what he thinks. At the moment however we have assumed that each plugin would be a singleton, and exist only once in the broker.</p>

      1. Jan 30, 2008

        <p>Perhaps point 1 could be swapped around to take Table as an optional argument. You would need to pass a Table object to a row if that row had been serialized and then unserialized, as there'd be no connection to its parent table.</p>

        <p>For point 3, I think being more specific is better - the plugin should be accepting an event from the row, rather than applying its own logic as to the 'state' of the row.</p>

        <p>I like the approach of point 4. Essentially you'd be defining an 'options' array for each event within the plugin. So your plugins would be accepting an 'options' array as an argument - it's up to each plugin handler to determine how that options list is processed. As an API I like it because it will really open the possibilities to provide config-driven table definitions.</p>

        1. Feb 11, 2008

          <p>Regarding point 1, I'm not sure it should be the plugins job to reconnect the row and the table; and thinking about it, I'm not sure that a row that's not connected to a table could ever get parsed to a plugin anyway, could it? Also, if a row has no table connected, how will the plugin broker know which table object to parse to the plugin?</p>

  2. Jan 31, 2008

    <p>@Simon:</p>

    <p>IMO, callbacks should be event-based only... not event-state-based.</p>

    <p>Outside ZF:<br />
    Think about an onClick() event in Javascript. The event doesn't care which element is calling it nor the state of that element. If that information is important then you pass the state into the callback: onClick(this);.</p>

    <p>In ZF:<br />
    The save() process itself is a SINGLE event whereas the state of the Row (both before and after the actual save) should carry little importance to which callbacks are fired once the process is complete. If the state does matter (which, I think would be the less common case) then there is the ability to check it within the callbacks.</p>

    <p>Also, with having two callbacks based on a single event you are setting yourself up for a lot of repetition. Not to mention more ambiguity within your code if both preInsert() and preUpdate() have to call a 3rd user-defined method to share common code from both events:</p>

    <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
    public function preInsert($row)
    {
    $this->_doPreSave($row);
    }

    public function preUpdate($row)
    {
    $this->_doPreSave($row);
    }

    protected function _doPreSave($row)
    {
    // logic
    }

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

    1. Jan 31, 2008

      <p>Hi Brandon</p>

      <p>I'm not proposing it is state-based. The plugin is simply responding to events fired from within the row itself, and there will definitely be a need for developers to have callbacks for these specific events.</p>

      <p>Having said that, it's not going to hurt to support all three events - pre/postSave, pre/postInsert and pre/postUpdate. I don't see any problem with xxxSave performing 'generic' validation/modification to the row data and then for xxxInsert or xxxUpdate to perform specific functions. It's up to the developer to work out which is more handy to use.</p>

      <p>Jack, are you OK with this?</p>

      1. Feb 11, 2008

        <p>Hi Simon, Brandon,<br />
        Sorry for the late reply, I've been really busy with work recently.</p>

        <p>I see no problem adding pre/postSave handlers, and can certainly see the advantage of having them. I'll add them to the proposal. However I do still think that the plug-in API should match the events of Zend_Db_Table(_Row), which currently have no such event methods. I think it would be beneficial to add pre/postSave handlers to the other table classes as well.</p>

  3. Feb 12, 2008

    <p>Super idea.<br />
    I can suggest one more feature to be integrated here: audit column data changes. It can be useful for some projects (like WiKi) to track field changes automatically at the special storage (db table or file). So this feature can be realised using preUpdate and postUpdate events.</p>

  4. Feb 23, 2008

    <p>Idea++</p>

    <p>Looks good, would love to see this for 1.6.</p>

  5. Mar 01, 2008

    <p>If I understood it correctly, a Plugin can currently only work inside a Db_Table, it has no possibility to add functions to the interface of a Db_Table/Db_Table_Row class. This could be needed for some plugins.</p>

    <p>As reference you may also look at doctrine plugins:
    <a class="external-link" href="http://www.phpdoctrine.org/documentation/manual/trunk/?chapter=plugins">http://www.phpdoctrine.org/documentation/manual/trunk/?chapter=plugins</a></p>

    1. Mar 01, 2008

      <p>Hi Niko - can you describe how you'd want the API of a table changed? I have my own personal preference on why I'd not want this to happen, but I could be talked out of it if there were a solid use case for it.</p>

      <p>E.g. whilst I don't think it's a great idea, it could be possible for the __call method of a table/row to access a separate event in the plugin, so the plugin could take responsibility for extending the API. But I'm a little unsure as to the impact of this performance-wise... Any other opinions on this?</p>

      1. Mar 02, 2008

        <p>Just a thought, but don't other ZF components add methods using helpers rather than plugins (ie. Zend_Controller has both plugins and action helpers)? The last thing I want is to overcomplicate things, but perhaps there's an argument for a "Zend_Db_Helper"?</p>

        <p>P.S. Simon, did you get my email regarding the get/setColumn methods? Should I update the proposal?</p>

  6. Jun 02, 2008

    <ac:macro ac:name="note"><ac:parameter ac:name="title">Zend Comments</ac:parameter><ac:rich-text-body>
    <p>The Zend Framework team approves this proposal for immediate development in the<br />
    standard incubator, with the following requirements/suggestions:</p>
    <ul>
    <li>Add a setConfig() method to Zend_Db_Table_Plugin_Abstract<br />
    This method should proxy to setOptions(), and accept a Zend_Config object; please follow the example set by Zend_Form and family. The main reason for this request is consistency in the framework.</li>
    <li>Plugin Broker should use Zend_Loader_PluginLoader. You may already be planning this, but it is unclear from the use cases and class skeletons. When used, you should also provide an accessor for the PluginLoader, to allow custom plugin loaders as required by end users.</li>
    <li>No indication of where/when the broker is attached to the table class<br />
    The table class should have public accessors for the broker (at minimum, getPluginBroker()).</li>
    <li>registerPlugin() should allow concrete instances (unclear from use cases).<br />
    Again, this is for a consistent story with plugins; you can look at Zend_Form for a representative implementation (specifically, addElement(), addDecorator(), etc.).</li>
    </ul>
    </ac:rich-text-body></ac:macro>

  7. Aug 28, 2008

    <p>First I really like this idea and thanks to the great work so far, I have even created a little image uploading/transformation plugin that is inspired by UploadColumn and PaperClip for ruby on rails, if you would like to try it out it is hosted at <a href="http://gitorious.org/projects/gem">gitorious</a>, it is in a early face so expect it to be broken.</p>

    <p>However I found some problem when I put together a couple of plugins and where calling save (or any other method that is calling notify internally) on a row object from a different table that did not had any plugins registred. I then found out that notify where trying to notify my plugins for all the tables that where involved in the request.</p>

    <p>Not sure if I might have misunderstood the expected behavior, but attached is a patch that solves the above.<br class="atl-forced-newline" /></p>
    <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
    Index: Zend/Db/Table/Plugin/Broker.php
    ===================================================================
    — Zend/Db/Table/Plugin/Broker.php    (revision 11111)
    +++ Zend/Db/Table/Plugin/Broker.php    (working copy)
    @@ -140,7 +140,7 @@
             }
            
             foreach ((array) $className as $class)

    Unknown macro: {-            self}

         }
     
    @@ -193,16 +193,20 @@
     
         /**
          * Notify plugins of an event
    -     *
    -     * @param  Zend_Loader_PluginLoader_Interface $loader
    +     *
    +     * @param Zend_Db_Table_Abstract $className
    +     * @param string $suffix
    +     * @param array $args Arguments that should be passed to the plugin
    +     * @return integer|boolean Returns number of plugins notified or false if
    +     *                         a plugin returns false.
          */
         static public function notify($className = null, $suffix = null, Array &$args = array())
         {
    -        $plugins = Zend_Db_Table_Plugin_Broker::getPlugins($className);
    +        if (false === self::hasTablePlugins($className))

    Unknown macro: {+            return 0; // The table has no plugins.+        }

     
    -        if (!$plugins)

    Unknown macro: {-            return false;-        }

    +        $plugins = Zend_Db_Table_Plugin_Broker::getTablePlugins($className);
     
             $method     = array_shift($args);
             $ret        = count($plugins);
    @@ -368,4 +372,23 @@
     
             return self::getPlugin($plugins);
         }
    +
    +    /**
    +     * Does a table have any plugins?
    +     *
    +     * @param string|Zend_Db_Table_Abstract $tableClass
    +     * @return boolean
    +     */
    +    static public function hasTablePlugins($tableClass)
    +    {
    +        if ($tableClass instanceof Zend_Db_Table_Abstract)

    Unknown macro: {+            $tableClass = get_class($tableClass);+        }

    +
    +        if (isset(self::$_table[$tableClass]))

    Unknown macro: {+            return true;+        }

    +        return false;
    +    }
    +   
     }
    ]]></ac:plain-text-body></ac:macro>

  8. Mar 18, 2009

    <p>This proposal has not been updated in the past 6 months. Archiving for now.</p>

  9. Apr 23, 2009

    <p>What is happening with this proposition, It would be cool to have this feature in ZF !</p>