View Source

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{zone-template-instance:ZFDEV:Zend Proposal Zone Template}

{zone-data:component-name}
Zend_Db_Table_Plugin
{zone-data}

{zone-data:proposer-list}
[Simon Mundy|mailto:simon.mundy@peptolab.com], [Jack Sleight|mailto:jack.sleight@gmail.com]
[Matthew Weier O'Phinney (Zend Liaison)|~matthew]
{zone-data}

{zone-data:revision}
0.2 - 23 February 2008
0.1 - 9 December 2007
{zone-data}

{zone-data: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.
{zone-data}

{zone-data:references}
* [Zend_Db|http://framework.zend.com/manual/en/zend.db.html]
* [Zend_Controller Plugins|http://framework.zend.com/manual/en/zend.controller.plugins.html]
{zone-data}

{zone-data:requirements}
* 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
{zone-data}

{zone-data:dependencies}
* Zend_Exception
* Zend_Db_Table
{zone-data}

{zone-data: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.
{zone-data}

{zone-data:milestones}
* 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
{zone-data}

{zone-data:class-list}
* Zend_Db_Table_Plugin_Interface
* Zend_Db_Table_Plugin_Broker
* Zend_Db_Table_Plugin_Abstract
* Zend_Db_Table_Plugin_Exception
{zone-data}

{zone-data:use-cases}
{composition-setup}
{deck:id=Usecase}
{card:label=UC 01 : Global plugin}
h3. Automatic created/modified dates
Global plugin applied to all/any Zend_Db_Table instances.
{code}
class My_Plugin_Date extends Zend_Db_Table_Plugin_Abstract
{
public function preInsertRow(Zend_Db_Table_Row_Abstract $row)
{
$date = Zend_Date::now();

if (isset($row->created)) {
$row->created = $date->getIso();
}
if (isset($row->modified)) {
$row->modified = $date->getIso();
}
}

public function preUpdateRow(Zend_Db_Table_Row_Abstract $row)
{
$date = Zend_Date::now();

if (isset($row->modified)) {
$row->modified = $date->getIso();
}
}
}

Zend_Db_Table_Plugin_Broker::addPrefix('My_Plugin');
Zend_Db_Table_Plugin_Broker::addPath('/path/to/My/Plugin');
Zend_Db_Table_Plugin_Broker::registerPlugin('Date');
{code}
{card}
{card:label=UC 02 : Per-table setup}
h3. Registering plugins within model definition

{code}
class Article extends Zend_Db_Table_Abstract
{
protected function _setupPlugins()
{
$this->registerPlugin('Date');
}
}

// OR

class Article extends Zend_Db_Table_Abstract
{
protected $_plugins = array('Date');
}

Zend_Db_Table_Plugin_Broker::addPrefix('My_Plugin');
Zend_Db_Table_Plugin_Broker::addPath('/path/to/My/Plugin');
{code}
{card}
{card:label=UC 03 : Auditing data changes}
h3. Tracking data changes with an audit logger

{code}
class My_Plugin_Audit extends Zend_Db_Table_Plugin_Abstract
{
protected $_log;

public function __construct($options = null)
{
$this->setOptions($options);

if (isset($this->_options['log'])) {
$this->_log = new Zend_Log();
foreach ((array) $this->_options['log'] as $option) {
if (is_array($option)) {
$className = key($option);
$options = (array) value($option);
} else {
$className = $option;
$options = array();
}
Zend_Loader::loadClass($className);
$class = new ReflectionClass($className);
$logger = $class->newInstanceArgs(array_values($options));
$this->_log->addWriter($logger);
}
}
}

public function preUpdateRow(Zend_Db_Table_Row_Abstract $row)
{
// Note to self: Must allow access to 'clean' data in Row to avoid this hack!
$clone = clone $row;
$dirty = $clone->toArray();
$clone->refresh();
$clean = $clone->toArray();

if ($dirty !== $clean) {
$this->_log()->info(get_class($clone). ' updated');
foreach (array_diff_assoc($dirty, $clean) as $key => $value) {
$this->_log()->info('Column ' . $key . ': ' . $clean[$key] . ' => ' . $value);
}
}
}
}

Zend_Db_Table_Plugin_Broker::addPrefix('My_Plugin');
Zend_Db_Table_Plugin_Broker::addPath('/path/to/My/Plugin');
Zend_Db_Table_Plugin_Broker::registerPlugin('Audit', array(
'log' => array(
'Zend_Log_Writer_Stream' => '../application/default/log/audit.log'
)
));
{code}
{card}
{card:label=UC 04 : Caching results}
h3. Caching fetch/find operations

{code}
class My_Plugin_Cache extends Zend_Db_Table_Plugin_Abstract
{
protected $_cache;

public function __construct($options = null)
{
$this->setOptions($options);

if (isset($this->_options['cache'])) {
$this->setCache($this->_options['cache']);
}
}

public function setCache(Zend_Cache_Core $cache)
{
$this->_cache = $cache;
return $this;
}

public function preFetchTable(Zend_Db_Table_Abstract $table, Zend_Db_Table_Select $select)
{
// If cached query exists, cancel the fetch operation
// NOTE: Cached rows will not have a table connection and must be re-set individually with setTable()
if ($result = $this->_cache->load($select->__toString())) {
return $result;
}
}

public function postFetchTable(Zend_Db_Table_Abstract $table, Zend_Db_Table_Select $select, $result)
{
$this->_cache->save($select->__toString(), $result);
}
}

$frontendOptions = array(
'lifetime' => 7200, // cache lifetime of 2 hours
'automatic_serialization' => true
);

$backendOptions = array(
'cache_dir' => './tmp/' // Directory where to put the cache files
);

Zend_Db_Table_Plugin_Broker::addPrefix('My_Plugin');
Zend_Db_Table_Plugin_Broker::addPath('/path/to/My/Plugin');
Zend_Db_Table_Plugin_Broker::registerPlugin('Cache', array(
'cache' => Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions)
));
{code}
{card}
{deck}
{zone-data}

{zone-data:skeletons}
{code}
interface Zend_Db_Table_Plugin_Interface
{
public function __construct(array $options = array());

public function preInitTable(Zend_Db_Table_Abstract $table);
public function postInitTable(Zend_Db_Table_Abstract $table);

public function preInitRowset(Zend_Db_Table_Rowset_Abstract $rowset);
public function postInitRowset(Zend_Db_Table_Rowset_Abstract $rowset);

public function preInitRow(Zend_Db_Table_Row_Abstract $row);
public function postInitRow(Zend_Db_Table_Row_Abstract $row);

public function preFetchTable(Zend_Db_Table_Abstract $table, Zend_Db_Table_Select $select);
public function postFetchTable(Zend_Db_Table_Abstract $table, Zend_Db_Table_Select $select, $result);

public function preSaveTable(Zend_Db_Table_Abstract $table, array $data);
public function postSaveTable(Zend_Db_Table_Abstract $table, array $data, $result);

public function preInsertTable(Zend_Db_Table_Abstract $table, array $data);
public function postInsertTable(Zend_Db_Table_Abstract $table, array $data, $result);

public function preUpdateTable(Zend_Db_Table_Abstract $table, array $data, $where);
public function postUpdateTable(Zend_Db_Table_Abstract $table, array $data, $where, $result);

public function preDeleteTable(Zend_Db_Table_Abstract $table, $where);
public function postDeleteTable(Zend_Db_Table_Abstract $table, $where, $result);

public function preSaveRow(Zend_Db_Table_Row_Abstract $row);
public function postSaveRow(Zend_Db_Table_Row_Abstract $row, $result);

public function preInsertRow(Zend_Db_Table_Row_Abstract $row);
public function postInsertRow(Zend_Db_Table_Row_Abstract $row, $result);

public function preUpdateRow(Zend_Db_Table_Row_Abstract $row);
public function postUpdateRow(Zend_Db_Table_Row_Abstract $row, $result);

public function preDeleteRow(Zend_Db_Table_Row_Abstract $row);
public function postDeleteRow(Zend_Db_Table_Row_Abstract $row, $result);

public function getColumn(Zend_Db_Table_Row_Abstract $row, $columnName);
public function setColumn(Zend_Db_Table_Row_Abstract $row, $columnName, $value);
}

abstract class Zend_Db_Table_Plugin_Abstract implements Zend_Db_Table_Plugin_Interface
{
protected $_options = array();

public function __construct($options = array())
{
$this->setOptions($options);
}

public function setOptions($options = array())
{
if ($options instanceof Zend_Config) {
$options = $options->toArray();
}

if (!is_array($options)) {
throw new Zend_Db_Table_Plugin_Exception('Invalid options');
}

$this->_options = $options;
}

public function getOptions()
{
return $this->_options;
}

// ...implements all defined methods in interface as 'empty' methods
}

class Zend_Db_Table_Plugin_Broker
{
const GLOBAL = 'global';
const PREPEND = 'prepend';
const APPEND = 'append';
const BEFORE_PLUGIN = 'before';
const AFTER_PLUGIN = 'after';

public function addPrefix($prefix)
{
}

public function addPath($path)
{
}

public function addPrefixPath($prefix, $path)
{
}

public function registerPlugin($plugin, $className = self::GLOBAL, $position = self::APPEND, $positionKey = null)
{
}

public function unregisterPlugin($plugin)
{
}

public function hasPlugin($className)
{
}

public function getPlugin($className)
{
}
}

class Zend_Db_Table_Plugin_Exception extends Zend_Db_Table_Extension
{
}
{code}
{zone-data}

{zone-template-instance}]]></ac:plain-text-body></ac:macro>