Labels
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: 16) |
Table of Contents
- 1. Overview
- 2. References
- 3. Component Requirements, Constraints, and Acceptance Criteria
- 4. Dependencies on Other Framework Components
- 5. Theory of Operation
- 6. Milestones / Tasks
- 7. Class Index
- 8. Use Cases
- Automatic created/modified dates
- Registering plugins within model definition
- Tracking data changes with an audit logger
- Caching fetch/find operations
- 9. Class Skeletons
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
Hi Brandon,
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.
1. I agree, no need to overcomplicate the API.
2. Good point, my mistake ![]()
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.
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.
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.
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.
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.
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?
@Simon:
IMO, callbacks should be event-based only... not event-state-based.
Outside ZF:
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);.
In ZF:
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.
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:
Hi Brandon
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.
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.
Jack, are you OK with this?
Hi Simon, Brandon,
Sorry for the late reply, I've been really busy with work recently.
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.
Super idea.
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.
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.
As reference you may also look at doctrine plugins:
http://www.phpdoctrine.org/documentation/manual/trunk/?chapter=plugins
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.
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?
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.S. Simon, did you get my email regarding the get/setColumn methods? Should I update the proposal?
| Zend Comments The Zend Framework team approves this proposal for immediate development in the
|
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 gitorious, it is in a early face so expect it to be broken.
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.
Not sure if I might have misunderstood the expected behavior, but attached is a patch that solves the above.
ZF Home Page
Code Browser
Wiki Dashboard
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.
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();
2. Zend_Db_Table_Plugin_Interface::getRowColumn doens't need the 3rd param "$value":
public function getRowColumn(Zend_Db_Table_Row_Abstract $row, $columnName);
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?
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: