From f747e97f1709eccfdb634088499b65c7c7dd00a3 Mon Sep 17 00:00:00 2001 From: Beat Date: Sun, 21 Jul 2013 19:09:27 +0200 Subject: [PATCH] #+ [#31488] Add Observer pattern to JTable (and use it for Tags implementation (which also fixes [#31339] too). Thanks Beat. (Fix #1561) --- .../com_categories/models/category.php | 12 +- .../components/com_contact/tables/contact.php | 34 +- .../com_newsfeeds/tables/newsfeed.php | 34 +- .../com_weblinks/tables/weblink.php | 33 +- components/com_tags/views/tag/view.html.php | 36 +- installation/CHANGELOG | 1 + libraries/cms.php | 8 + libraries/cms/helper/tags.php | 17 +- libraries/joomla/observer/index.html | 1 + libraries/joomla/observer/mapper.php | 325 ++++++++++++++++++ libraries/joomla/table/nested.php | 35 +- libraries/joomla/table/observer.php | 112 ++++++ libraries/joomla/table/observer/index.html | 1 + libraries/joomla/table/observer/tags.php | 163 +++++++++ libraries/joomla/table/table.php | 209 +++++++---- libraries/legacy/model/admin.php | 11 +- libraries/legacy/table/category.php | 35 +- libraries/legacy/table/content.php | 42 +-- 18 files changed, 837 insertions(+), 272 deletions(-) create mode 100644 libraries/joomla/observer/index.html create mode 100644 libraries/joomla/observer/mapper.php create mode 100644 libraries/joomla/table/observer.php create mode 100644 libraries/joomla/table/observer/index.html create mode 100644 libraries/joomla/table/observer/tags.php diff --git a/administrator/components/com_categories/models/category.php b/administrator/components/com_categories/models/category.php index d1d92bbfe9e2c..cf74ca8f440df 100644 --- a/administrator/components/com_categories/models/category.php +++ b/administrator/components/com_categories/models/category.php @@ -674,14 +674,14 @@ protected function batchTag($value, $pks, $contexts) $table->reset(); $table->load($pk); $tags = array($value); - //$typeAlias = $table->get('tagsHelper')->typeAlias; - $typeAlias = $table->extension . '.category'; - $table->get('tagsHelper')->typeAlias = $typeAlias; - $oldTags = $table->get('tagsHelper')->getTagIds($pk, $typeAlias); - $table->get('tagsHelper')->oldTags = $oldTags; + /** + * @var JTableObserverTags $tagsObserver + */ + $tagsObserver = $table->getObserverOfClass('JTableObserverTags'); + $result = $tagsObserver->setNewTags($tags, false); - if (!$table->get('tagsHelper')->postStoreProcess($table, $tags, false)) + if (!$result) { $this->setError($table->getError()); diff --git a/administrator/components/com_contact/tables/contact.php b/administrator/components/com_contact/tables/contact.php index 253f059f99e24..b42d52e707c7c 100644 --- a/administrator/components/com_contact/tables/contact.php +++ b/administrator/components/com_contact/tables/contact.php @@ -15,14 +15,6 @@ */ class ContactTableContact extends JTable { - /** - * Helper object for storing and deleting tag information. - * - * @var JHelperTags - * @since 3.1 - */ - protected $tagsHelper = null; - /** * Constructor * @@ -33,8 +25,6 @@ class ContactTableContact extends JTable public function __construct(&$db) { parent::__construct('#__contact_details', 'id', $db); - $this->tagsHelper = new JHelperTags; - $this->tagsHelper->typeAlias = 'com_contact.contact'; } /** @@ -65,22 +55,6 @@ public function bind($array, $ignore = '') return parent::bind($array, $ignore); } - /** - * Override parent delete method to delete tags information. - * - * @param integer $pk Primary key to delete. - * - * @return boolean True on success. - * - * @since 3.1 - * @throws UnexpectedValueException - */ - public function delete($pk = null) - { - $result = parent::delete($pk); - return $result && $this->tagsHelper->deleteTagData($this, $pk); - } - /** * Stores a contact * @@ -156,12 +130,8 @@ public function store($updateNulls = false) return false; } - $this->tagsHelper->preStoreProcess($this); - $result = parent::store($updateNulls); - - $this->newTags = isset($this->newTags) ? $this->newTags : array(); - return $result && $this->tagsHelper->postStoreProcess($this, $this->newTags); - } + return parent::store($updateNulls); + } /** * Overloaded check function diff --git a/administrator/components/com_newsfeeds/tables/newsfeed.php b/administrator/components/com_newsfeeds/tables/newsfeed.php index 9e57a962d5e02..b48cf0fe0e3f1 100644 --- a/administrator/components/com_newsfeeds/tables/newsfeed.php +++ b/administrator/components/com_newsfeeds/tables/newsfeed.php @@ -15,14 +15,6 @@ */ class NewsfeedsTableNewsfeed extends JTable { - /** - * Helper object for storing and deleting tag information. - * - * @var JHelperTags - * @since 3.1 - */ - protected $tagsHelper = null; - /** * Constructor * @@ -31,8 +23,6 @@ class NewsfeedsTableNewsfeed extends JTable public function __construct(&$db) { parent::__construct('#__newsfeeds', 'id', $db); - $this->tagsHelper = new JHelperTags; - $this->tagsHelper->typeAlias = 'com_newsfeeds.newsfeed'; } /** @@ -133,23 +123,6 @@ public function check() return true; } - /** - * Override parent delete method to delete tags information. - * - * @param integer $pk Primary key to delete. - * - * @return boolean True on success. - * - * @since 3.1 - * @throws UnexpectedValueException - */ - public function delete($pk = null) - { - $result = parent::delete($pk); - $this->tagsHelper->typeAlias = 'com_newsfeeds.newsfeed'; - return $result && $this->tagsHelper->deleteTagData($this, $pk); - } - /** * Overriden JTable::store to set modified data. * @@ -193,11 +166,6 @@ public function store($updateNulls = false) // Save links as punycode. $this->link = JStringPunycode::urlToPunycode($this->link); - $this->tagsHelper->typeAlias = 'com_newsfeeds.newsfeed'; - $this->tagsHelper->preStoreProcess($this); - $result = parent::store($updateNulls); - - $this->newTags = isset($this->newTags) ? $this->newTags : array(); - return $result && $this->tagsHelper->postStoreProcess($this, $this->newTags); + return parent::store($updateNulls); } } diff --git a/administrator/components/com_weblinks/tables/weblink.php b/administrator/components/com_weblinks/tables/weblink.php index 604f5acc40481..a72438140a797 100644 --- a/administrator/components/com_weblinks/tables/weblink.php +++ b/administrator/components/com_weblinks/tables/weblink.php @@ -18,14 +18,6 @@ */ class WeblinksTableWeblink extends JTable { - /** - * Helper object for storing and deleting tag information. - * - * @var JHelperTags - * @since 3.1 - */ - protected $tagsHelper = null; - /** * Constructor * @@ -34,9 +26,6 @@ class WeblinksTableWeblink extends JTable public function __construct(&$db) { parent::__construct('#__weblinks', 'id', $db); - - $this->tagsHelper = new JHelperTags; - $this->tagsHelper->typeAlias = 'com_weblinks.weblink'; } /** @@ -132,11 +121,7 @@ public function store($updateNulls = false) // Convert IDN urls to punycode $this->url = JStringPunycode::urlToPunycode($this->url); - $this->tagsHelper->preStoreProcess($this); - $result = parent::store($updateNulls); - - $this->newTags = isset($this->newTags) ? $this->newTags : array(); - return $result && $this->tagsHelper->postStoreProcess($this, $this->newTags); + return parent::store($updateNulls); } /** @@ -209,22 +194,6 @@ public function check() return true; } - /** - * Override parent delete method to delete tags information. - * - * @param integer $pk Primary key to delete. - * - * @return boolean True on success. - * - * @since 3.1 - * @throws UnexpectedValueException - */ - public function delete($pk = null) - { - $result = parent::delete($pk); - return $result && $this->tagsHelper->deleteTagData($this, $pk); - } - /** * Method to set the publishing state for a row or list of rows in the database * table. The method respects checked out rows by other users and will attempt diff --git a/components/com_tags/views/tag/view.html.php b/components/com_tags/views/tag/view.html.php index 652734cf265e1..886d5d5b388b6 100644 --- a/components/com_tags/views/tag/view.html.php +++ b/components/com_tags/views/tag/view.html.php @@ -69,33 +69,35 @@ public function display($tpl = null) $itemElement->params->merge($temp); $itemElement->params = (array) json_decode($itemElement->params); } - foreach ($items as $itemElement) + if ($items !== false) { - $itemElement->event = new stdClass; + foreach ($items as $itemElement) + { + $itemElement->event = new stdClass; - // For some plugins. - !empty($itemElement->core_body)? $itemElement->text = $itemElement->core_body : $itemElement->text = null; + // For some plugins. + !empty($itemElement->core_body)? $itemElement->text = $itemElement->core_body : $itemElement->text = null; - $dispatcher = JEventDispatcher::getInstance(); + $dispatcher = JEventDispatcher::getInstance(); - JPluginHelper::importPlugin('content'); - $dispatcher->trigger('onContentPrepare', array ('com_tags.tag', &$itemElement, &$itemElement->core_params, 0)); + JPluginHelper::importPlugin('content'); + $dispatcher->trigger('onContentPrepare', array ('com_tags.tag', &$itemElement, &$itemElement->core_params, 0)); - $results = $dispatcher->trigger('onContentAfterTitle', array('com_tags.tag', &$itemElement, &$itemElement->core_params, 0)); - $itemElement->event->afterDisplayTitle = trim(implode("\n", $results)); + $results = $dispatcher->trigger('onContentAfterTitle', array('com_tags.tag', &$itemElement, &$itemElement->core_params, 0)); + $itemElement->event->afterDisplayTitle = trim(implode("\n", $results)); - $results = $dispatcher->trigger('onContentBeforeDisplay', array('com_tags.tag', &$itemElement, &$itemElement->core_params, 0)); - $itemElement->event->beforeDisplayContent = trim(implode("\n", $results)); + $results = $dispatcher->trigger('onContentBeforeDisplay', array('com_tags.tag', &$itemElement, &$itemElement->core_params, 0)); + $itemElement->event->beforeDisplayContent = trim(implode("\n", $results)); - $results = $dispatcher->trigger('onContentAfterDisplay', array('com_tags.tag', &$itemElement, &$itemElement->core_params, 0)); - $itemElement->event->afterDisplayContent = trim(implode("\n", $results)); + $results = $dispatcher->trigger('onContentAfterDisplay', array('com_tags.tag', &$itemElement, &$itemElement->core_params, 0)); + $itemElement->event->afterDisplayContent = trim(implode("\n", $results)); - if ($itemElement->text) - { - $itemElement->core_body = $itemElement->text; + if ($itemElement->text) + { + $itemElement->core_body = $itemElement->text; + } } } - } $this->state = &$state; diff --git a/installation/CHANGELOG b/installation/CHANGELOG index 1780147da62df..d46e3488a65a5 100644 --- a/installation/CHANGELOG +++ b/installation/CHANGELOG @@ -29,6 +29,7 @@ $ -> Language fix or change 21-Jul-2013 Jean-Marie Simonet + [#31349] Display Error Message when Magic Quotes is Enabled. Thanks Brian Teeman + #+ [#31488] Add Observer pattern to JTable (and use it for Tags implementation (which also fixes [#31339] too). Thanks Beat. 20-Jul-2013 Jean-Marie Simonet # [#31496] DEBUG_BACKTRACE_IGNORE_ARGS not supported in PHP 5.3.1-5.3.5. Thanks Beat diff --git a/libraries/cms.php b/libraries/cms.php index 316f5c4eb2d89..cfa3a223088fa 100644 --- a/libraries/cms.php +++ b/libraries/cms.php @@ -58,3 +58,11 @@ JLoader::register('JInstallerPlugin', JPATH_PLATFORM . '/cms/installer/adapter/plugin.php'); JLoader::register('JInstallerTemplate', JPATH_PLATFORM . '/cms/installer/adapter/template.php'); JLoader::register('JExtension', JPATH_PLATFORM . '/cms/installer/extension.php'); + +// Register Observers: +// Add Tags to Content, Contact, NewsFeeds, WebLinks and Categories: (this is the only link between them here!): +JObserverMapper::addObserverClassToClass('JTableObserverTags', 'JTableContent', array('typeAlias' => 'com_content.article')); +JObserverMapper::addObserverClassToClass('JTableObserverTags', 'ContactTableContact', array('typeAlias' => 'com_contact.contact')); +JObserverMapper::addObserverClassToClass('JTableObserverTags', 'NewsfeedsTableNewsfeed', array('typeAlias' => 'com_newsfeeds.newsfeed')); +JObserverMapper::addObserverClassToClass('JTableObserverTags', 'WeblinksTableWeblink', array('typeAlias' => 'com_weblinks.weblink')); +JObserverMapper::addObserverClassToClass('JTableObserverTags', 'JTableCategory', array('typeAlias' => '{extension}.category')); diff --git a/libraries/cms/helper/tags.php b/libraries/cms/helper/tags.php index 46e53b67d54b4..3af87156c432a 100644 --- a/libraries/cms/helper/tags.php +++ b/libraries/cms/helper/tags.php @@ -347,6 +347,9 @@ public function deleteTagData(JTable $table, $contentItemId) { $result = $this->unTagItem($contentItemId, $table); + /** + * @var JTableCorecontent $ucmContentTable + */ $ucmContentTable = JTable::getInstance('Corecontent'); return $result && $ucmContentTable->deleteByContentId($contentItemId); @@ -757,17 +760,22 @@ public static function getTypes($arrayType = 'objectList', $selectTypes = null, * @param array $newTags Array of new tags * @param boolean $replace Flag indicating if all exising tags should be replaced * - * @return null + * @return boolean * * @since 3.1 */ public function postStoreProcess($table, $newTags = array(), $replace = true) { + if (!empty($table->newTags) && empty($newTags)) + { + $newTags = $table->newTags; + } + // If existing row, check to see if tags have changed. $newTable = clone $table; $newTable->reset(); $key = $newTable->getKeyName(); - $typeAlias = $newTable->get('tagsHelper')->typeAlias; + $typeAlias = $this->typeAlias; $result = true; @@ -784,6 +792,7 @@ public function postStoreProcess($table, $newTags = array(), $replace = true) { // Process the tags $rowdata = new JHelperContent; + $data = $rowdata->getRowData($table); $ucmContentTable = JTable::getInstance('Corecontent'); @@ -825,7 +834,7 @@ public function preStoreProcess($table, $newTags = array()) $oldTable = clone $table; $oldTable->reset(); $key = $oldTable->getKeyName(); - $typeAlias = $oldTable->get('tagsHelper')->typeAlias; + $typeAlias = $this->typeAlias; if ($oldTable->$key && $oldTable->load()) { @@ -962,7 +971,7 @@ public function tagDeleteInstances($tag_id) public function tagItem($ucmId, $table, $tags = array(), $replace = true) { $key = $table->get('_tbl_key'); - $oldTags = $table->get('tagsHelper')->getTagIds((int) $table->$key, $table->get('tagsHelper')->typeAlias); + $oldTags = $this->getTagIds((int) $table->$key, $this->typeAlias); $oldTags = explode(',', $oldTags); $result = $this->unTagItem($ucmId, $table); diff --git a/libraries/joomla/observer/index.html b/libraries/joomla/observer/index.html new file mode 100644 index 0000000000000..2efb97f319a35 --- /dev/null +++ b/libraries/joomla/observer/index.html @@ -0,0 +1 @@ + diff --git a/libraries/joomla/observer/mapper.php b/libraries/joomla/observer/mapper.php new file mode 100644 index 0000000000000..f53f15ae9e2e3 --- /dev/null +++ b/libraries/joomla/observer/mapper.php @@ -0,0 +1,325 @@ +_observers = new JObserverUpdater($this); + * JObserverMapper::attachAllObservers($this); + * + * 3) add the function attachObserver below to your class to add observers using the JObserverUpdater class: + * public function attachObserver(JObserverInterface $observer) + * { + * $this->_observers->attachObserver($observer); + * } + * + * 4) in the methods that need to be observed, add, e.g. (name of event, params of event): + * $this->_observers->update('onBeforeLoad', array($keys, $reset)); + * + * @package Joomla + * @subpackage Observer + * @link http://docs.joomla.org/JObservableInterface + * @since 3.1.2 + */ +interface JObservableInterface +{ + /** + * Adds an observer to this JObservableInterface instance. + * Ideally, this method should be called fron the constructor of JObserverInterface + * which should be instanciated by JObserverMapper. + * The implementation of this function can use JObserverUpdater + * + * @param JObserverInterface $observer The observer to attach to $this observable subject + * + * @return void + */ + public function attachObserver(JObserverInterface $observer); +} + +/** + * Observer pattern interface for Joomla + * + * A class that wants to observe another class must: + * + * 1) Add: implements JObserverInterface + * to its class + * + * 2) Implement a constructor, that can look like this: + * public function __construct(JObservableInterface $observableObject) + * { + * $observableObject->attachObserver($this); + * $this->observableObject = $observableObject; + * } + * + * 3) and must implement the instanciator function createObserver() below, e.g. as follows: + * public static function createObserver(JObservableInterface $observableObject, $params = array()) + * { + * $observer = new self($observableObject); + * $observer->... = $params['...']; ... + * return $observer; + * } + * + * 4) Then add functions corresponding to the events to be observed, + * E.g. to respond to event: $this->_observers->update('onBeforeLoad', array($keys, $reset)); + * following function is needed in the obser: + * public function onBeforeLoad($keys, $reset) { ... } + * + * 5) Finally, the binding is made outside the observable and observer classes, using: + * JObserverMapper::addObserverClassToClass('ObserverClassname', 'ObservableClassname', array('paramName' => 'paramValue')); + * where the last array will be provided to the observer instanciator function createObserver. + * + * @package Joomla + * @subpackage Observer + * @link http://docs.joomla.org/JObserverInterface + * @since 3.1.2 + */ +interface JObserverInterface +{ + /** + * Creates the associated observer instance and attaches it to the $observableObject + * + * @param JObservableInterface $observableObject The observable subject object + * @param array $params Params for this observer + * + * @return JObserverInterface + */ + public static function createObserver(JObservableInterface $observableObject, $params = array()); +} + +/** + * Observer updater pattern implementation for Joomla + * + * @package Joomla + * @subpackage Observer + * @link http://docs.joomla.org/JObserverUpdater + * @since 3.1.2 + */ +interface JObserverUpdaterInterface +{ + /** + * Constructor + * + * @param JObservableInterface $observable The observable subject object + */ + public function __construct(JObservableInterface $observable); + + /** + * Adds an observer to the JObservableInterface instance updated by this + * This method can be called fron JObservableInterface::attachObserver + * + * @param JObserverInterface $observer The observer object + * + * @return void + */ + public function attachObserver(JObserverInterface $observer); + + /** + * Call all observers for $event with $params + * + * @param string $event Event name (function name in observer) + * @param array $params Params of event (params in observer function) + * + * @return void + */ + public function update($event, $params); + + /** + * Enable/Disable calling of observers (this is useful when calling parent:: function + * + * @param boolean $enabled Enable (true) or Disable (false) the observer events + * + * @return boolean Returns old state + */ + public function doCallObservers($enabled); +} + +/** + * Observer updater pattern implementation for Joomla + * + * @package Joomla + * @subpackage Observer + * @link http://docs.joomla.org/JObserverUpdater + * @since 3.1.2 + */ +class JObserverUpdater implements JObserverUpdaterInterface +{ + /** + * Generic JObserverInterface observers for this JObservableInterface + * + * @var JObserverInterface[] + */ + protected $observers = array(); + + /** + * Process observers (useful when a class extends significantly an observerved method, and calls observers itself + * @var boolean + */ + protected $doCallObservers = true; + + /** + * Constructor + * + * @param JObservableInterface $observable The observable subject object + */ + public function __construct(JObservableInterface $observable) + { + // Not yet needed, but possible: $this->observable = $observable; + } + + /** + * Adds an observer to the JObservableInterface instance updated by this + * This method can be called fron JObservableInterface::attachObserver + * + * @param JObserverInterface $observer The observer object + * + * @return void + */ + public function attachObserver(JObserverInterface $observer) + { + $this->observers[get_class($observer)] = $observer; + } + + /** + * Gets the instance of the observer of class $observerClass + * + * @param string $observerClass The class name of the observer + * + * @return JTableObserver|null The observer object of this class if any + */ + public function getObserverOfClass($observerClass) + { + if (isset($this->observers[$observerClass])) + { + return $this->observers[$observerClass]; + } + + return null; + } + + /** + * Call all observers for $event with $params + * + * @param string $event Name of the event + * @param array $params Params of the event + * + * @return void + */ + public function update($event, $params) + { + if ($this->doCallObservers) + { + foreach ($this->observers as $observer) + { + $eventListener = array($observer, $event); + + if (is_callable($eventListener)) + { + call_user_func_array($eventListener, $params); + } + } + } + } + + /** + * Enable/Disable calling of observers (this is useful when calling parent:: function + * + * @param boolean $enabled Enable (true) or Disable (false) the observer events + * + * @return boolean Returns old state + */ + public function doCallObservers($enabled) + { + $oldState = $this->doCallObservers; + $this->doCallObservers = $enabled; + + return $oldState; + } +} + +/** + * Observer mapping pattern implementation for Joomla + * + * @package Joomla + * @subpackage Observer + * @link http://docs.joomla.org/JObserverMapper + * @since 3.1.2 + */ +class JObserverMapper +{ + /** + * Array: array( JObservableInterface_classname => array( JObserverInterface_classname => array( paramname => param, .... ) ) ) + * + * @var array[] + */ + protected static $observations = array(); + + /** + * Adds a mapping to observe $observerClass subjects with $observableClass observer/listener, attaching it on creation with $params + * on $observableClass instance creations + * + * @param string $observerClass The name of the observer class (implementing JObserverInterface) + * @param string $observableClass The name of the observable class (implementing JObservableInterface) + * @param array|boolean $params The params to give to the JObserverInterface::createObserver() function, or false to remove mapping + * + * @return void + */ + public static function addObserverClassToClass($observerClass, $observableClass, $params = array()) + { + if ($params !== false) + { + static::$observations[$observableClass][$observerClass] = $params; + } + else + { + unset(static::$observations[$observableClass][$observerClass]); + } + } + + /** + * Attaches all applicable observers to an $observableObject + * + * @param JObservableInterface $observableObject The observable subject object + * + * @return void + */ + public static function attachAllObservers(JObservableInterface $observableObject) + { + $observableClass = get_class($observableObject); + + while ($observableClass != false) + { + // Attach applicable Observers for the class to the Observable subject: + if (isset(static::$observations[$observableClass])) + { + foreach (static::$observations[$observableClass] as $observerClass => $params) + { + // Attach an Observer to the Observable subject: + /** + * @var JObserverInterface $observerClass + */ + $observerClass::createObserver($observableObject, $params); + } + } + + // Get parent class name (or false if none), and redo the above on it: + $observableClass = get_parent_class($observableClass); + } + } +} diff --git a/libraries/joomla/table/nested.php b/libraries/joomla/table/nested.php index 9f7f146bc8792..f848ed7384f9c 100644 --- a/libraries/joomla/table/nested.php +++ b/libraries/joomla/table/nested.php @@ -698,6 +698,9 @@ public function store($updateNulls = false) { $k = $this->_tbl_key; + // Implement JObservableInterface: Pre-processing by observers + $this->_observers->update('onBeforeStore', array($updateNulls, $k)); + // @codeCoverageIgnoreStart if ($this->_debug) { @@ -819,23 +822,32 @@ public function store($updateNulls = false) } // Store the row to the database. - if (!parent::store($updateNulls)) - { - $this->_unlock(); - return false; - } - // @codeCoverageIgnoreStart - if ($this->_debug) + // Implement JObservableInterface: We do not want parent::store to update observers, + // since tables are locked and we are updating it from this level of store(): + $oldCallObservers = $this->_observers->doCallObservers(false); + + $result = parent::store($updateNulls); + + // Implement JObservableInterface: Restore previous callable observers state: + $this->_observers->doCallObservers($oldCallObservers); + + if ($result) { - $this->_logtable(); + // @codeCoverageIgnoreStart + if ($this->_debug) + { + $this->_logtable(); + } + // @codeCoverageIgnoreEnd } - // @codeCoverageIgnoreEnd - // Unlock the table for writing. $this->_unlock(); - return true; + // Implement JObservableInterface: Post-processing by observers + $this->_observers->update('onAfterStore', array(&$result)); + + return $result; } /** @@ -854,6 +866,7 @@ public function store($updateNulls = false) * * @link http://docs.joomla.org/JTableNested/publish * @since 11.1 + * @throws UnexpectedValueException */ public function publish($pks = null, $state = 1, $userId = 0) { diff --git a/libraries/joomla/table/observer.php b/libraries/joomla/table/observer.php new file mode 100644 index 0000000000000..7e72efc11365d --- /dev/null +++ b/libraries/joomla/table/observer.php @@ -0,0 +1,112 @@ +attachObserver($this); + $this->table = $table; + } + + /** + * Pre-processor for $table->load($keys, $reset) + * + * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. If not + * set the instance property value is used. + * @param boolean $reset True to reset the default values before loading the new row. + * + * @return void + */ + public function onBeforeLoad($keys, $reset) + { + } + + /** + * Post-processor for $table->load($keys, $reset) + * + * @param boolean &$result The result of the load + * @param array $row The loaded (and already binded to $this->table) row of the database table + * + * @return void + */ + public function onAfterLoad(&$result, $row) + { + } + + /** + * Pre-processor for $table->store($updateNulls) + * + * @param boolean $updateNulls The result of the load + * @param string $tableKey The key of the table + * + * @return void + */ + public function onBeforeStore($updateNulls, $tableKey) + { + } + + /** + * Post-processor for $table->store($updateNulls) + * + * @param boolean &$result The result of the store + * + * @return void + */ + public function onAfterStore(&$result) + { + } + + /** + * Pre-processor for $table->delete($pk) + * + * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. + * @param string $tableKey The normal key of the table + * + * @return void + * + * @throws UnexpectedValueException + */ + public function onBeforeDelete($pk, $tableKey) + { + } + + /** + * Post-processor for $table->delete($pk) + * + * @param mixed $pk The deleted primary key value. + * + * @return void + */ + public function onAfterDelete($pk) + { + } +} diff --git a/libraries/joomla/table/observer/index.html b/libraries/joomla/table/observer/index.html new file mode 100644 index 0000000000000..2efb97f319a35 --- /dev/null +++ b/libraries/joomla/table/observer/index.html @@ -0,0 +1 @@ + diff --git a/libraries/joomla/table/observer/tags.php b/libraries/joomla/table/observer/tags.php new file mode 100644 index 0000000000000..e4c2faefc3941 --- /dev/null +++ b/libraries/joomla/table/observer/tags.php @@ -0,0 +1,163 @@ + $typeAlias ) + * + * @return JObserverInterface|JTableObserverTags + */ + public static function createObserver(JObservableInterface $observableObject, $params = array()) + { + $typeAlias = $params['typeAlias']; + + $observer = new self($observableObject); + + $observer->tagsHelper = new JHelperTags; + $observer->typeAliasPattern = $typeAlias; + + return $observer; + } + + /** + * Pre-processor for $table->store($updateNulls) + * + * @param boolean $updateNulls The result of the load + * @param string $tableKey The key of the table + * + * @return void + */ + public function onBeforeStore($updateNulls, $tableKey) + { + $this->parseTypeAlias(); + $this->tagsHelper->preStoreProcess($this->table); + } + + /** + * Post-processor for $table->store($updateNulls) + * You can change optional params newTags and replaceTags of tagsHelper with method setNewTagsToAdd + * + * @param boolean &$result The result of the load + * + * @return void + */ + public function onAfterStore(&$result) + { + if ($result) + { + $result = $this->tagsHelper->postStoreProcess($this->table); + + // Restore default values for the optional params: + $this->newTags = array(); + $this->replaceTags = true; + } + } + + /** + * Pre-processor for $table->delete($pk) + * + * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. + * @param string $tableKey The normal key of the table + * + * @return void + * + * @throws UnexpectedValueException + */ + public function onBeforeDelete($pk, $tableKey) + { + $this->parseTypeAlias(); + $this->tagsHelper->deleteTagData($this->table, $pk); + } + + /** + * Sets the new tags to be added/replaced to the table row + * + * @param array $newTags New tags to be added or replaced + * @param boolean $replaceTags Replace tags (true) or add them (false) + * + * @return boolean + */ + public function setNewTags($newTags, $replaceTags) + { + $this->parseTypeAlias(); + + return $this->tagsHelper->postStoreProcess($this->table, $newTags, $replaceTags); + } + + /** + * Internal method + * Parses a TypeAlias of the form "{variableName}.type", replacing {variableName} with table-instance variables variableName + * Storing result into $this->tagsHelper->typeAlias + * + * @return void + */ + protected function parseTypeAlias() + { + // Needed for PHP < 5.4.0 as it's not passing context $this to closure function + static::$_myTableForPregreplaceOnly = $this->table; + + $this->tagsHelper->typeAlias = preg_replace_callback('/{([^}]+)}/', + function($matches) { return JTableObserverTags::$_myTableForPregreplaceOnly->{$matches[1]}; }, + $this->typeAliasPattern + ); + } +} diff --git a/libraries/joomla/table/table.php b/libraries/joomla/table/table.php index 2bc82150a3199..3c5cd68c4c9df 100644 --- a/libraries/joomla/table/table.php +++ b/libraries/joomla/table/table.php @@ -22,7 +22,7 @@ * @since 11.1 * @tutorial Joomla.Platform/jtable.cls */ -abstract class JTable extends JObject +abstract class JTable extends JObject implements JObservableInterface { /** * Include paths for searching for JTable classes. @@ -80,6 +80,14 @@ abstract class JTable extends JObject */ protected $_locked = false; + /** + * Generic observers for this JTable (Used e.g. for tags Processing) + * + * @var JObserverUpdater + * @since 3.1.2 + */ + protected $_observers; + /** * Object constructor to set table and key fields. In most cases this will * be overridden by child classes to explicitly set the table and key fields @@ -123,6 +131,42 @@ public function __construct($table, $key, $db) { $this->access = (int) JFactory::getConfig()->get('access'); } + + // Implement JObservableInterface: + // Create observer updater and attaches all observers interested by $this class: + $this->_observers = new JObserverUpdater($this); + JObserverMapper::attachAllObservers($this); + } + + /** + * Implement JObservableInterface: + * Adds an observer to this instance. + * This method will be called fron the constructor of classes implementing JObserverInterface + * which is instanciated by the constructor of $this with JObserverMapper::attachAllObservers($this) + * + * @param JObserverInterface|JTableObserver $observer The observer object + * + * @return void + * + * @since 3.1.2 + */ + public function attachObserver(JObserverInterface $observer) + { + $this->_observers->attachObserver($observer); + } + + /** + * Gets the instance of the observer of class $observerClass + * + * @param string $observerClass The observer class-name to return the object of + * + * @return JTableObserver|null + * + * @since 3.1.2 + */ + public function getObserverOfClass($observerClass) + { + return $this->_observers->getObserverOfClass($observerClass); } /** @@ -428,7 +472,7 @@ public function reset() * * @link http://docs.joomla.org/JTable/bind * @since 11.1 - * @throws UnexpectedValueException + * @throws InvalidArgumentException */ public function bind($src, $ignore = array()) { @@ -483,6 +527,9 @@ public function bind($src, $ignore = array()) */ public function load($keys = null, $reset = true) { + // Implement JObservableInterface: Pre-processing by observers + $this->_observers->update('onBeforeLoad', array($keys, $reset)); + if (empty($keys)) { // If empty, use the value of the current key @@ -532,11 +579,18 @@ public function load($keys = null, $reset = true) // Check that we have a result. if (empty($row)) { - return false; + $result = false; } + else + { + // Bind the object with the row and return. + $result = $this->bind($row); + } + + // Implement JObservableInterface: Post-processing by observers + $this->_observers->update('onAfterLoad', array(&$result, $row)); - // Bind the object with the row and return. - return $this->bind($row); + return $result; } /** @@ -572,6 +626,10 @@ public function check() public function store($updateNulls = false) { $k = $this->_tbl_key; + + // Implement JObservableInterface: Pre-processing by observers + $this->_observers->update('onBeforeStore', array($updateNulls, $k)); + if (!empty($this->asset_id)) { $currentAssetId = $this->asset_id; @@ -591,84 +649,90 @@ public function store($updateNulls = false) // If a primary key exists update the object, otherwise insert it. if ($this->$k) { - $this->_db->updateObject($this->_tbl, $this, $this->_tbl_key, $updateNulls); + $result = $this->_db->updateObject($this->_tbl, $this, $this->_tbl_key, $updateNulls); } else { - $this->_db->insertObject($this->_tbl, $this, $this->_tbl_key); + $result = $this->_db->insertObject($this->_tbl, $this, $this->_tbl_key); } // If the table is not set to track assets return true. - if (!$this->_trackAssets) - { - return true; - } - - if ($this->_locked) + if ($this->_trackAssets) { - $this->_unlock(); - } - /* - * Asset Tracking - */ + if ($this->_locked) + { + $this->_unlock(); + } - $parentId = $this->_getAssetParentId(); - $name = $this->_getAssetName(); - $title = $this->_getAssetTitle(); + /* + * Asset Tracking + */ - $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); - $asset->loadByName($name); + $parentId = $this->_getAssetParentId(); + $name = $this->_getAssetName(); + $title = $this->_getAssetTitle(); - // Re-inject the asset id. - $this->asset_id = $asset->id; + $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); + $asset->loadByName($name); - // Check for an error. - $error = $asset->getError(); - if ($error) - { - $this->setError($error); - return false; - } + // Re-inject the asset id. + $this->asset_id = $asset->id; - // Specify how a new or moved node asset is inserted into the tree. - if (empty($this->asset_id) || $asset->parent_id != $parentId) - { - $asset->setLocation($parentId, 'last-child'); - } + // Check for an error. + $error = $asset->getError(); + if ($error) + { + $this->setError($error); + $result = false; + } + else + { + // Specify how a new or moved node asset is inserted into the tree. + if (empty($this->asset_id) || $asset->parent_id != $parentId) + { + $asset->setLocation($parentId, 'last-child'); + } - // Prepare the asset to be stored. - $asset->parent_id = $parentId; - $asset->name = $name; - $asset->title = $title; + // Prepare the asset to be stored. + $asset->parent_id = $parentId; + $asset->name = $name; + $asset->title = $title; - if ($this->_rules instanceof JAccessRules) - { - $asset->rules = (string) $this->_rules; - } + if ($this->_rules instanceof JAccessRules) + { + $asset->rules = (string) $this->_rules; + } - if (!$asset->check() || !$asset->store($updateNulls)) - { - $this->setError($asset->getError()); - return false; + if (!$asset->check() || !$asset->store($updateNulls)) + { + $this->setError($asset->getError()); + $result = false; + } + else + { + // Create an asset_id or heal one that is corrupted. + if (empty($this->asset_id) || ($currentAssetId != $this->asset_id && !empty($this->asset_id))) + { + // Update the asset_id field in this table. + $this->asset_id = (int) $asset->id; + + $query = $this->_db->getQuery(true) + ->update($this->_db->quoteName($this->_tbl)) + ->set('asset_id = ' . (int) $this->asset_id) + ->where($this->_db->quoteName($k) . ' = ' . (int) $this->$k); + $this->_db->setQuery($query); + + $this->_db->execute(); + } + } + } } - // Create an asset_id or heal one that is corrupted. - if (empty($this->asset_id) || ($currentAssetId != $this->asset_id && !empty($this->asset_id))) - { - // Update the asset_id field in this table. - $this->asset_id = (int) $asset->id; - - $query = $this->_db->getQuery(true) - ->update($this->_db->quoteName($this->_tbl)) - ->set('asset_id = ' . (int) $this->asset_id) - ->where($this->_db->quoteName($k) . ' = ' . (int) $this->$k); - $this->_db->setQuery($query); - - $this->_db->execute(); - } + // Implement JObservableInterface: Post-processing by observers + $this->_observers->update('onAfterStore', array(&$result)); - return true; + return $result; } /** @@ -729,7 +793,7 @@ public function save($src, $orderingFilter = '', $ignore = '') } /** - * Override parent delete method to delete tags information. + * Deletes this row in database (or if provided, the row of key $pk) * * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. * @@ -742,6 +806,10 @@ public function save($src, $orderingFilter = '', $ignore = '') public function delete($pk = null) { $k = $this->_tbl_key; + + // Implement JObservableInterface: Pre-processing by observers + $this->_observers->update('onBeforeDelete', array($pk, $k)); + $pk = (is_null($pk)) ? $this->$k : $pk; // If no primary key is given, return false. @@ -754,6 +822,8 @@ public function delete($pk = null) if ($this->_trackAssets) { // Get and the asset name. + $savedK = $this->$k; + $this->$k = $pk; $name = $this->_getAssetName(); $asset = self::getInstance('Asset'); @@ -771,6 +841,8 @@ public function delete($pk = null) $this->setError($asset->getError()); return false; } + + $this->$k = $savedK; } // Delete the row by primary key. @@ -782,6 +854,9 @@ public function delete($pk = null) // Check for a database error. $this->_db->execute(); + // Implement JObservableInterface: Post-processing by observers + $this->_observers->update('onAfterDelete', array($pk)); + return true; } @@ -801,6 +876,7 @@ public function delete($pk = null) * * @link http://docs.joomla.org/JTable/checkOut * @since 11.1 + * @throws UnexpectedValueException */ public function checkOut($userId, $pk = null) { @@ -848,6 +924,7 @@ public function checkOut($userId, $pk = null) * * @link http://docs.joomla.org/JTable/checkIn * @since 11.1 + * @throws UnexpectedValueException */ public function checkIn($pk = null) { @@ -972,6 +1049,7 @@ public function isCheckedOut($with = 0, $against = null) * * @link http://docs.joomla.org/JTable/getNextOrder * @since 11.1 + * @throws UnexpectedValueException */ public function getNextOrder($where = '') { @@ -1008,6 +1086,7 @@ public function getNextOrder($where = '') * * @link http://docs.joomla.org/JTable/reorder * @since 11.1 + * @throws UnexpectedValueException */ public function reorder($where = '') { diff --git a/libraries/legacy/model/admin.php b/libraries/legacy/model/admin.php index e8f69826d8b25..bb2d979c4d683 100644 --- a/libraries/legacy/model/admin.php +++ b/libraries/legacy/model/admin.php @@ -566,12 +566,13 @@ protected function batchTag($value, $pks, $contexts) $table->reset(); $table->load($pk); $tags = array($value); - $typeAlias = $table->get('tagsHelper')->typeAlias; - $oldTags = $table->get('tagsHelper')->getTagIds($pk, $typeAlias); - $table->get('tagsHelper')->oldTags = $oldTags; - - if (!$table->get('tagsHelper')->postStoreProcess($table, $tags, false)) + /** + * @var JTableObserverTags $tagsObserver + */ + $tagsObserver = $table->getObserverOfClass('JTableObserverTags'); + $result = $tagsObserver->setNewTags($tags, false); + if (!$result) { $this->setError($table->getError()); diff --git a/libraries/legacy/table/category.php b/libraries/legacy/table/category.php index a787d0068d32c..8d6bfccdd84aa 100644 --- a/libraries/legacy/table/category.php +++ b/libraries/legacy/table/category.php @@ -18,15 +18,6 @@ */ class JTableCategory extends JTableNested { - - /** - * Helper object for storing and deleting tag information. - * - * @var JHelperTags - * @since 3.1 - */ - protected $tagsHelper = null; - /** * Constructor * @@ -39,7 +30,6 @@ public function __construct($db) parent::__construct('#__categories', 'id', $db); $this->access = (int) JFactory::getConfig()->get('access'); - $this->tagsHelper = new JHelperTags; } /** @@ -196,24 +186,6 @@ public function bind($array, $ignore = '') return parent::bind($array, $ignore); } - /** - * Override parent delete method to process tags - * - * @param integer $pk The primary key of the node to delete. - * @param boolean $children True to delete child nodes, false to move them up a level. - * - * @return boolean True on success. - * - * @since 3.1 - * @throws UnexpectedValueException - */ - public function delete($pk = null, $children = true) - { - $result = parent::delete($pk); - $this->tagsHelper->typeAlias = $this->extension . '.category'; - return $result && $this->tagsHelper->deleteTagData($this, $pk); - } - /** * Overridden JTable::store to set created/modified and user id. * @@ -252,11 +224,6 @@ public function store($updateNulls = false) return false; } - $this->tagsHelper->typeAlias = $this->extension . '.category'; - $this->tagsHelper->preStoreProcess($this); - $result = parent::store($updateNulls); - - $this->newTags = isset($this->newTags) ? $this->newTags : array(); - return $result && $this->tagsHelper->postStoreProcess($this, $this->newTags); + return parent::store($updateNulls); } } diff --git a/libraries/legacy/table/content.php b/libraries/legacy/table/content.php index cbe98b0d5a183..784b894508e68 100644 --- a/libraries/legacy/table/content.php +++ b/libraries/legacy/table/content.php @@ -19,14 +19,6 @@ */ class JTableContent extends JTable { - /** - * Helper object for storing and deleting tag information. - * - * @var JHelperTags - * @since 3.1 - */ - protected $tagsHelper = null; - /** * Constructor * @@ -38,8 +30,14 @@ public function __construct($db) { parent::__construct('#__content', 'id', $db); - $this->tagsHelper = new JHelperTags; - $this->tagsHelper->typeAlias = 'com_content.article'; + // This is left here for reference: + // + // This would set up the tags observer in $this from here (so not entirely decoupled): + // JTableObserverTags::createObserver($this, array('typeAlias' => 'com_content.article')); + // + // But this makes the relation between content and tags completely external to Content as JTable is observable: + // So we are doing this only once in libraries/cms.php: + // JObserverFactory::addObserverClassToClass('JTableObserverTags', 'JTableContent', array('typeAlias' => 'com_content.article')); } /** @@ -240,23 +238,6 @@ public function check() return true; } - /** - * Override parent delete method to delete tags information. - * - * @param integer $pk Primary key to delete. - * - * @return boolean True on success. - * - * @since 3.1 - * @throws UnexpectedValueException - */ - public function delete($pk = null) - { - $result = parent::delete($pk); - $this->tagsHelper->typeAlias = 'com_content.article'; - return $result && $this->tagsHelper->deleteTagData($this, $pk); - } - /** * Overrides JTable::store to set modified data and user id. * @@ -302,12 +283,7 @@ public function store($updateNulls = false) return false; } - $this->tagsHelper->typeAlias = 'com_content.article'; - $this->tagsHelper->preStoreProcess($this); - $result = parent::store($updateNulls); - - $this->newTags = isset($this->newTags) ? $this->newTags : array(); - return $result && $this->tagsHelper->postStoreProcess($this, $this->newTags); + return parent::store($updateNulls); } /**