<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_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.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
2. References
3. Component Requirements, Constraints, and Acceptance Criteria
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
16 Comments
comments.show.hideDec 14, 2007
Brandon Parise
<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()
// ...
}
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())
$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>
Dec 14, 2007
Jack Sleight
<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>
Jan 30, 2008
Simon Mundy
<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>
Feb 11, 2008
Jack Sleight
<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>
Jan 31, 2008
Brandon Parise
<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>
Jan 31, 2008
Simon Mundy
<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>
Feb 11, 2008
Jack Sleight
<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>
Feb 12, 2008
Pavel Chipiga
<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>
Feb 23, 2008
Jurrien Stutterheim
<p>Idea++</p>
<p>Looks good, would love to see this for 1.6.</p>
Mar 01, 2008
Niko Sams
<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>
Mar 01, 2008
Simon Mundy
<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>
Mar 02, 2008
Jack Sleight
<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>
Jun 02, 2008
Matthew Weier O'Phinney
<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>
Aug 28, 2008
Johan Nilsson
<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)
}
@@ -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))
- if (!$plugins)
+ $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)
+
+ if (isset(self::$_table[$tableClass]))
+ return false;
+ }
+
}
]]></ac:plain-text-body></ac:macro>
Mar 18, 2009
Wil Sinclair
<p>This proposal has not been updated in the past 6 months. Archiving for now.</p>
Apr 23, 2009
Sylvain Filteau
<p>What is happening with this proposition, It would be cool to have this feature in ZF !</p>