Allow different history drivers. This still uses Horde_History as a base class. It...
authorGunnar Wrobel <p@rdus.de>
Tue, 29 Sep 2009 11:53:51 +0000 (13:53 +0200)
committerGunnar Wrobel <p@rdus.de>
Tue, 29 Sep 2009 11:53:51 +0000 (13:53 +0200)
framework/History/lib/Horde/History.php
framework/History/lib/Horde/History/Factory.php [new file with mode: 0644]
framework/History/lib/Horde/History/Sql.php [new file with mode: 0644]
framework/History/package.xml
framework/History/test/Horde/History/InterfaceTest.php

index d9ef516..5953889 100644 (file)
@@ -1,5 +1,23 @@
 <?php
 /**
+ * The Horde_History:: system.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  History
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=History
+ */
+
+/**
+ * The Autoloader allows us to omit "require/include" statements.
+ */
+require_once 'Horde/Autoloader.php';
+
+/**
  * The Horde_History:: class provides a method of tracking changes in Horde
  * objects, stored in a SQL table.
  *
  * See the enclosed file COPYING for license information (LGPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
  *
- * @author  Chuck Hagenbuch <chuck@horde.org>
- * @package Horde_History
+ * @category Horde
+ * @package  History
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=History
  */
 class Horde_History
 {
     /**
-     * Instance object.
-     *
-     * @var Horde_History
-     */
-    static protected $_instance;
-
-    /**
-     * Pointer to a DB instance to manage the history.
-     *
-     * @var DB
-     */
-    protected $_db;
-
-    /**
-     * Handle for the current database connection, used for writing. Defaults
-     * to the same handle as $_db if a separate write database is not required.
+     * Instance cache.
      *
-     * @var DB
+     * @var array
      */
-    protected $_write_db;
+    static protected $_instances;
 
     /**
      * Attempts to return a reference to a concrete History instance.
@@ -42,62 +49,44 @@ class Horde_History
      *
      * This method must be invoked as: $var = History::singleton()
      *
-     * @return Horde_History  The concrete Horde_History reference.
-     * @throws Horde_Exception
-     */
-    static public function singleton()
-    {
-        if (!isset(self::$_instance)) {
-            self::$_instance = new Horde_History();
-        }
-
-        return self::$_instance;
-    }
-
-    /**
-     * Constructor.
+     * @param string $driver The driver to use.
      *
+     * @return Horde_History  The concrete Horde_History reference.
      * @throws Horde_Exception
      */
-    public function __construct()
+    static public function singleton($driver = null)
     {
         global $conf;
 
-        if (empty($conf['sql']['phptype']) || ($conf['sql']['phptype'] == 'none')) {
-            throw new Horde_Exception(_("The History system is disabled."));
+        if (empty($driver)) {
+            $driver = 'Sql';
         }
 
-        $this->_write_db = DB::connect($conf['sql']);
-
-        /* Set DB portability options. */
-        $portability = DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS;
-
-        if (is_a($this->_write_db, 'DB_common')) {
-            $write_portability = $portability;
-            if ($this->_write_db->phptype == 'mssql') {
-                $write_portability |= DB_PORTABILITY_RTRIM;
+        if ($driver == 'Sql') {
+            if (empty($conf['sql']['phptype'])
+                || ($conf['sql']['phptype'] == 'none')) {
+                throw new Horde_Exception(_("The History system is disabled."));
             }
-            $this->_write_db->setOption('portability', $write_portability);
+            $params = $conf['sql'];
+        } else {
+            $params = array();
         }
 
-        /* Check if we need to set up the read DB connection
-         * seperately. */
-        if (!empty($conf['sql']['splitread'])) {
-            $params = array_merge($conf['sql'], $conf['sql']['read']);
-            $this->_db = DB::connect($params);
-
-            /* Set DB portability options. */
-            if (is_a($this->_db, 'DB_common')) {
-                $read_portability = $portability;
-                if ($this->_db->phptype == 'mssql') {
-                    $read_portability |= DB_PORTABILITY_RTRIM;
-                }
-                $this->_db->setOption('portability', $read_portability);
-            }
-        } else {
-            /* Default to the same DB handle for reads. */
-            $this->_db =& $this->_write_db;
+        if (!isset(self::$_instances[$driver])) {
+            $injector = new Horde_Injector(new Horde_Injector_TopLevel());
+            $injector->bindFactory(
+                'Horde_History',
+                'Horde_History_Factory',
+                'getHistory'
+            );
+            $config = new stdClass;
+            $config->driver = $driver;
+            $config->params = $params;
+            $injector->setInstance('Horde_History_Config', $config);
+            self::$_instances[$driver] = $injector->getInstance('Horde_History');
         }
+
+        return self::$_instances[$driver];
     }
 
     /**
@@ -111,18 +100,21 @@ class Horde_History
      *   'ts' => Timestamp of the action (this will be added automatically if
      *           it is not present).
      *
-     * @param string $guid            The unique identifier of the entry to
-     *                                add to.
-     * @param array $attributes       The hash of name => value entries that
-     *                                describe this event.
-     * @param boolean $replaceAction  If $attributes['action'] is already
-     *                                present in the item's history log,
-     *                                update that entry instead of creating a
-     *                                new one.
+     * @param string  $guid          The unique identifier of the entry to
+     *                               add to.
+     * @param array   $attributes    The hash of name => value entries that
+     *                               describe this event.
+     * @param boolean $replaceAction If $attributes['action'] is already
+     *                               present in the item's history log,
+     *                               update that entry instead of creating a
+     *                               new one.
+     *
+     * @return boolean True if the operation succeeded.
      *
      * @throws Horde_Exception
      */
-    public function log($guid, $attributes = array(), $replaceAction = false)
+    public function log($guid, array $attributes = array(),
+                        $replaceAction = false)
     {
         $history = $this->getHistory($guid);
 
@@ -133,145 +125,84 @@ class Horde_History
             $attributes['ts'] = time();
         }
 
-        /* If we want to replace an entry with the same action, try and find
-         * one. Track whether or not we succeed in $done, so we know whether
-         * or not to add the entry later. */
-        $done = false;
-        if ($replaceAction && !empty($attributes['action'])) {
-            for ($i = 0, $count = count($history->data); $i < $count; ++$i) {
-                if (!empty($history->data[$i]['action']) &&
-                    $history->data[$i]['action'] == $attributes['action']) {
-                    $values = array(
-                        $attributes['ts'],
-                        $attributes['who'],
-                        isset($attributes['desc']) ? $attributes['desc'] : null
-                    );
-
-                    unset($attributes['ts'], $attributes['who'], $attributes['desc'], $attributes['action']);
-
-                    $values[] = $attributes
-                        ? serialize($attributes)
-                        : null;
-                    $values[] = $history->data[$i]['id'];
-
-                    $r = $this->_write_db->query(
-                        'UPDATE horde_histories SET history_ts = ?,' .
-                        ' history_who = ?,' .
-                        ' history_desc = ?,' .
-                        ' history_extra = ? WHERE history_id = ?', $values
-                    );
-
-                    if ($r instanceof PEAR_Error) {
-                        Horde::logMessage($r, __FILE__, __LINE__, PEAR_LOG_ERR);
-                        throw new Horde_Exception($r->getMessage());
-                    }
-                    $done = true;
-                    break;
-                }
-            }
-        }
-
-        /* If we're not replacing by action, or if we didn't find an entry to
-         * replace, insert a new row. */
-        if (!$done) {
-            $history_id = $this->_write_db->nextId('horde_histories');
-            if ($history_id instanceof PEAR_Error) {
-                Horde::logMessage($history_id, __FILE__, __LINE__, PEAR_LOG_ERR);
-                throw new Horde_Exception($history_id->getMessage());
-            }
-
-            $values = array(
-                $history_id,
-                $guid,
-                $attributes['ts'],
-                $attributes['who'],
-                isset($attributes['desc']) ? $attributes['desc'] : null,
-                isset($attributes['action']) ? $attributes['action'] : null
-            );
-
-            unset($attributes['ts'], $attributes['who'], $attributes['desc'], $attributes['action']);
-
-            $values[] = $attributes
-                ? serialize($attributes)
-                : null;
-
-            $r = $this->_write_db->query(
-                'INSERT INTO horde_histories (history_id, object_uid, history_ts, history_who, history_desc, history_action, history_extra)' .
-                ' VALUES (?, ?, ?, ?, ?, ?, ?)', $values
-            );
-
-            if ($r instanceof PEAR_Error) {
-                Horde::logMessage($r, __FILE__, __LINE__, PEAR_LOG_ERR);
-                throw new Horde_Exception($r->getMessage());
-            }
-        }
+        return $this->_log($history, $attributes, $replaceAction);
+    }
 
-        return true;
+    /**
+     * Logs an event to an item's history log. Any other details about the event
+     * are passed in $attributes.
+     *
+     * @param Horde_HistoryObject $history       The history item to add to.
+     * @param array               $attributes    The hash of name => value entries
+     *                                           that describe this event.
+     * @param boolean             $replaceAction If $attributes['action'] is
+     *                                           already present in the item's
+     *                                           history log, update that entry
+     *                                           instead of creating a new one.
+     *
+     * @return boolean True if the operation succeeded.
+     *
+     * @throws Horde_Exception
+     */
+    protected function _log(Horde_HistoryObject $history, array $attributes,
+                            $replaceAction = false)
+    {
+        throw new Horde_Exception('Not implemented!');
     }
 
     /**
      * Returns a Horde_HistoryObject corresponding to the named history
      * entry, with the data retrieved appropriately.
      *
-     * @param string $guid  The name of the history entry to retrieve.
+     * @param string $guid The name of the history entry to retrieve.
+     *
+     * @return Horde_HistoryObject A Horde_HistoryObject
      *
-     * @return Horde_HistoryObject  A Horde_HistoryObject
+     * @throws Horde_Exception
      */
     public function getHistory($guid)
     {
-        $rows = $this->_db->getAll('SELECT * FROM horde_histories WHERE object_uid = ?', array($guid), DB_FETCHMODE_ASSOC);
-        return new Horde_HistoryObject($guid, $rows);
+        throw new Horde_Exception('Not implemented!');
     }
 
     /**
      * Finds history objects by timestamp, and optionally filter on other
      * fields as well.
      *
-     * @param string $cmp     The comparison operator (<, >, <=, >=, or =) to
-     *                        check the timestamps with.
-     * @param integer $ts     The timestamp to compare against.
-     * @param array $filters  An array of additional (ANDed) criteria.
-     *                        Each array value should be an array with 3
-     *                        entries:
+     * @param string  $cmp     The comparison operator (<, >, <=, >=, or =) to
+     *                         check the timestamps with.
+     * @param integer $ts      The timestamp to compare against.
+     * @param array   $filters An array of additional (ANDed) criteria.
+     *                         Each array value should be an array with 3
+     *                         entries:
      * <pre>
      * 'field' - the history field being compared (i.e. 'action').
      * 'op'    - the operator to compare this field with.
      * 'value' - the value to check for (i.e. 'add').
      * </pre>
-     * @param string $parent  The parent history to start searching at. If
-     *                        non-empty, will be searched for with a LIKE
-     *                        '$parent:%' clause.
+     * @param string  $parent  The parent history to start searching at. If
+     *                         non-empty, will be searched for with a LIKE
+     *                         '$parent:%' clause.
      *
      * @return array  An array of history object ids, or an empty array if
      *                none matched the criteria.
+     *
+     * @throws Horde_Exception
      */
-    public function getByTimestamp($cmp, $ts, $filters = array(),
+    public function getByTimestamp($cmp, $ts, array $filters = array(),
                                    $parent = null)
     {
-        /* Build the timestamp test. */
-        $where = array("history_ts $cmp $ts");
-
-        /* Add additional filters, if there are any. */
-        if ($filters) {
-            foreach ($filters as $filter) {
-                $where[] = 'history_' . $filter['field'] . ' ' . $filter['op'] . ' ' . $this->_db->quote($filter['value']);
-            }
-        }
-
-        if ($parent) {
-            $where[] = 'object_uid LIKE ' . $this->_db->quote($parent . ':%');
-        }
-
-        return $this->_db->getAssoc('SELECT DISTINCT object_uid, history_id FROM horde_histories WHERE ' . implode(' AND ', $where));
+        throw new Horde_Exception('Not implemented!');
     }
 
     /**
      * Gets the timestamp of the most recent change to $guid.
      *
-     * @param string $guid    The name of the history entry to retrieve.
-     * @param string $action  An action: 'add', 'modify', 'delete', etc.
+     * @param string $guid   The name of the history entry to retrieve.
+     * @param string $action An action: 'add', 'modify', 'delete', etc.
      *
      * @return integer  The timestamp, or 0 if no matching entry is found.
+     *
      * @throws Horde_Exception
      */
     public function getActionTimestamp($guid, $action)
@@ -301,20 +232,15 @@ class Horde_History
     /**
      * Remove one or more history entries by name.
      *
-     * @param array $names  The history entries to remove.
+     * @param array $names The history entries to remove.
+     *
+     * @return boolean True if the operation succeeded.
+     *
+     * @throws Horde_Exception
      */
-    public function removeByNames($names)
+    public function removeByNames(array $names)
     {
-        if (!count($names)) {
-            return true;
-        }
-
-        $ids = array();
-        foreach ($names as $name) {
-            $ids[] = $this->_write_db->quote($name);
-        }
-
-        return $this->_write_db->query('DELETE FROM horde_histories WHERE object_uid IN (' . implode(',', $ids) . ')');
+        throw new Horde_Exception('Not implemented!');
     }
 
 }
diff --git a/framework/History/lib/Horde/History/Factory.php b/framework/History/lib/Horde/History/Factory.php
new file mode 100644 (file)
index 0000000..c475912
--- /dev/null
@@ -0,0 +1,137 @@
+<?php
+/**
+ * A factory for history handlers.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  History
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=History
+ */
+
+/**
+ * The Autoloader allows us to omit "require/include" statements.
+ */
+require_once 'Horde/Autoloader.php';
+
+/**
+ * The Horde_History_Factory:: provides a method for generating
+ * a Horde_History handler.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  History
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=History
+ */
+class Horde_History_Factory
+{
+    /**
+     * Create a concrete Horde_History instance.
+     *
+     * @param Horde_Injector $injector The environment for creating the instance.
+     *
+     * @return Horde_History The new Horde_History instance.
+     *
+     * @throws Horde_Exception If the injector provides no configuration.
+     */
+    static public function getHistory(Horde_Injector $injector)
+    {
+        try {
+            $config = $injector->getInstance('Horde_History_Config');
+        } catch (ReflectionException $e) {
+            throw new Horde_Exception(
+                sprintf(
+                    'The configuration for the History driver is missing: %s',
+                    $e->getMessage()
+                )
+            );
+        }
+
+        switch (ucfirst($config->driver)) {
+        case 'Sql':
+            return Horde_History_Factory::getHistorySql($injector, $config->params);
+        case 'Mock':
+        default:
+            return Horde_History_Factory::getHistoryMock($config->params);
+        }
+    }
+
+    /**
+     * Create a concrete Horde_History_Sql instance.
+     *
+     * @param Horde_Injector $injector The environment for creating the instance.
+     * @param array          $params   The db connection parameters if the
+     *                                 environment does not already provide a 
+     *                                 connection.
+     *
+     * @return Horde_History_Sql The new Horde_History_Sql instance.
+     *
+     * @throws Horde_Exception If the injector provides no configuration or
+     *                         creating the database connection failed.
+     */
+    static protected function getHistorySql(Horde_Injector $injector, array $params)
+    {
+        try {
+            /** See if there is a specific write db instance available */
+            $write_db = $injector->getInstance('DB_common_write');
+            $history = new Horde_History_Sql($write_db);
+            try {
+                /** See if there is a specific read db instance available */
+                $read_db = $injector->getInstance('DB_common_read');
+                $history->setReadDb($read_db);
+            } catch (ReflectionException $e) {
+            }
+        } catch (ReflectionException $e) {
+            /** No DB instances. Use the configuration. */
+            $write_db = Horde_History_Factory::getDb($params);
+
+            $history = new Horde_History_Sql($write_db);
+
+            /* Check if we need to set up the read DB connection
+             * seperately. */
+            if (!empty($params['splitread'])) {
+                $params  = array_merge($params, $params['read']);
+                $read_db = Horde_History_Factory::getDb($params);
+                $history->setReadDb($read_db);
+            }
+        }
+        return $history;
+    }
+
+    /**
+     * Create a database connection.
+     *
+     * @param array $params The database connection parameters.
+     *
+     * @return DB_common
+     *
+     * @throws Horde_Exception In case the database connection failed.
+     */
+    static protected function getDb(array $params)
+    {
+        $db = DB::connect($params);
+
+        /* Set DB portability options. */
+        $portability = DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS;
+
+        if ($db instanceOf DB_common) {
+            if ($db->phptype == 'mssql') {
+                $portability |= DB_PORTABILITY_RTRIM;
+            }
+            $db->setOption('portability', $portability);
+        } else if ($db instanceOf PEAR_Error) {
+            throw new Horde_Exception($db->getMessage());
+        }
+        return $db;
+    }
+}
\ No newline at end of file
diff --git a/framework/History/lib/Horde/History/Sql.php b/framework/History/lib/Horde/History/Sql.php
new file mode 100644 (file)
index 0000000..91bdd62
--- /dev/null
@@ -0,0 +1,283 @@
+<?php
+/**
+ * A sql based history driver.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  History
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=History
+ */
+
+/**
+ * The Horde_History_Sql:: class provides a method of tracking changes in Horde
+ * objects, stored in a SQL table.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  History
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=History
+ */
+class Horde_History_Sql extends Horde_History
+{
+    /**
+     * Pointer to a DB instance to manage the history.
+     *
+     * @var DB
+     */
+    protected $_db;
+
+    /**
+     * Handle for the current database connection, used for writing. Defaults
+     * to the same handle as $_db if a separate write database is not required.
+     *
+     * @var DB
+     */
+    protected $_write_db;
+
+    /**
+     * Constructor.
+     *
+     * @param DB_common $db The database connection.
+     */
+    public function __construct(DB_common $db)
+    {
+        $this->_write_db = $db;
+        $this->_db       = $db;
+    }
+
+    /**
+     * Set a separate read database connection if you want to split read and
+     * write access to the db.
+     *
+     * @param DB_common $db The database connection.
+     *
+     * @return NULL
+     */
+    public function setReadDb(DB_common $db)
+    {
+        $this->_db = $db;
+    }
+
+    /**
+     * Logs an event to an item's history log. Any other details about the event
+     * are passed in $attributes.
+     *
+     * @param Horde_HistoryObject $history       The history item to add to.
+     * @param array               $attributes    The hash of name => value entries
+     *                                           that describe this event.
+     * @param boolean             $replaceAction If $attributes['action'] is
+     *                                           already present in the item's
+     *                                           history log, update that entry
+     *                                           instead of creating a new one.
+     *
+     * @return boolean True if the operation succeeded.
+     *
+     * @throws Horde_Exception
+     */
+    protected function _log(Horde_HistoryObject $history,
+                            array $attributes,
+                            $replaceAction = false)
+    {
+        /* If we want to replace an entry with the same action, try and find
+         * one. Track whether or not we succeed in $done, so we know whether
+         * or not to add the entry later. */
+        $done = false;
+        if ($replaceAction && !empty($attributes['action'])) {
+            for ($i = 0, $count = count($history->data); $i < $count; ++$i) {
+                if (!empty($history->data[$i]['action']) &&
+                    $history->data[$i]['action'] == $attributes['action']) {
+                    $values = array(
+                        $attributes['ts'],
+                        $attributes['who'],
+                        isset($attributes['desc']) ? $attributes['desc'] : null
+                    );
+
+                    unset($attributes['ts'], $attributes['who'],
+                          $attributes['desc'], $attributes['action']);
+
+                    $values[] = $attributes
+                        ? serialize($attributes)
+                        : null;
+                    $values[] = $history->data[$i]['id'];
+
+                    $r = $this->_write_db->query(
+                        'UPDATE horde_histories SET history_ts = ?,' .
+                        ' history_who = ?,' .
+                        ' history_desc = ?,' .
+                        ' history_extra = ? WHERE history_id = ?', $values
+                    );
+
+                    if ($r instanceof PEAR_Error) {
+                        Horde::logMessage($r, __FILE__, __LINE__, PEAR_LOG_ERR);
+                        throw new Horde_Exception($r->getMessage());
+                    }
+                    $done = true;
+                    break;
+                }
+            }
+        }
+
+        /* If we're not replacing by action, or if we didn't find an entry to
+         * replace, insert a new row. */
+        if (!$done) {
+            $history_id = $this->_write_db->nextId('horde_histories');
+            if ($history_id instanceof PEAR_Error) {
+                Horde::logMessage($history_id, __FILE__, __LINE__, PEAR_LOG_ERR);
+                throw new Horde_Exception($history_id->getMessage());
+            }
+
+            $values = array(
+                $history_id,
+                $history->uid,
+                $attributes['ts'],
+                $attributes['who'],
+                isset($attributes['desc']) ? $attributes['desc'] : null,
+                isset($attributes['action']) ? $attributes['action'] : null
+            );
+
+            unset($attributes['ts'], $attributes['who'],
+                  $attributes['desc'], $attributes['action']);
+
+            $values[] = $attributes
+                ? serialize($attributes)
+                : null;
+
+            $r = $this->_write_db->query(
+                'INSERT INTO horde_histories (history_id, object_uid, history_ts, history_who, history_desc, history_action, history_extra)' .
+                ' VALUES (?, ?, ?, ?, ?, ?, ?)', $values
+            );
+
+            if ($r instanceof PEAR_Error) {
+                Horde::logMessage($r, __FILE__, __LINE__, PEAR_LOG_ERR);
+                throw new Horde_Exception($r->getMessage());
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns a Horde_HistoryObject corresponding to the named history
+     * entry, with the data retrieved appropriately.
+     *
+     * @param string $guid The name of the history entry to retrieve.
+     *
+     * @return Horde_HistoryObject  A Horde_HistoryObject
+     */
+    public function getHistory($guid)
+    {
+        $rows = $this->_db->getAll('SELECT * FROM horde_histories WHERE object_uid = ?', array($guid), DB_FETCHMODE_ASSOC);
+        return new Horde_HistoryObject($guid, $rows);
+    }
+
+    /**
+     * Finds history objects by timestamp, and optionally filter on other
+     * fields as well.
+     *
+     * @param string  $cmp     The comparison operator (<, >, <=, >=, or =) to
+     *                         check the timestamps with.
+     * @param integer $ts      The timestamp to compare against.
+     * @param array   $filters An array of additional (ANDed) criteria.
+     *                         Each array value should be an array with 3
+     *                         entries:
+     * <pre>
+     * 'field' - the history field being compared (i.e. 'action').
+     * 'op'    - the operator to compare this field with.
+     * 'value' - the value to check for (i.e. 'add').
+     * </pre>
+     * @param string  $parent  The parent history to start searching at. If
+     *                         non-empty, will be searched for with a LIKE
+     *                         '$parent:%' clause.
+     *
+     * @return array  An array of history object ids, or an empty array if
+     *                none matched the criteria.
+     *
+     * @throws Horde_Exception
+     */
+    public function getByTimestamp($cmp, $ts, $filters = array(),
+                                   $parent = null)
+    {
+        /* Build the timestamp test. */
+        $where = array("history_ts $cmp $ts");
+
+        /* Add additional filters, if there are any. */
+        if ($filters) {
+            foreach ($filters as $filter) {
+                $where[] = 'history_' . $filter['field'] . ' ' . $filter['op'] . ' ' . $this->_db->quote($filter['value']);
+            }
+        }
+
+        if ($parent) {
+            $where[] = 'object_uid LIKE ' . $this->_db->quote($parent . ':%');
+        }
+
+        return $this->_db->getAssoc('SELECT DISTINCT object_uid, history_id FROM horde_histories WHERE ' . implode(' AND ', $where));
+    }
+
+    /**
+     * Gets the timestamp of the most recent change to $guid.
+     *
+     * @param string $guid   The name of the history entry to retrieve.
+     * @param string $action An action: 'add', 'modify', 'delete', etc.
+     *
+     * @return integer  The timestamp, or 0 if no matching entry is found.
+     *
+     * @throws Horde_Exception
+     */
+    public function getActionTimestamp($guid, $action)
+    {
+        /* This implementation still works, but we should be able to
+         * get much faster now with a SELECT MAX(history_ts)
+         * ... query. */
+        try {
+            $history = $this->getHistory($guid);
+        } catch (Horde_Exception $e) {
+            return 0;
+        }
+
+        $last = 0;
+
+        if (is_array($history->data)) {
+            foreach ($history->data as $entry) {
+                if (($entry['action'] == $action) && ($entry['ts'] > $last)) {
+                    $last = $entry['ts'];
+                }
+            }
+        }
+
+        return (int)$last;
+    }
+
+    /**
+     * Remove one or more history entries by name.
+     *
+     * @param array $names The history entries to remove.
+     *
+     * @return boolean True if the operation succeeded.
+     *
+     * @throws Horde_Exception
+     */
+    public function removeByNames($names)
+    {
+        if (!count($names)) {
+            return true;
+        }
+
+        $ids = array();
+        foreach ($names as $name) {
+            $ids[] = $this->_write_db->quote($name);
+        }
+
+        return $this->_write_db->query('DELETE FROM horde_histories WHERE object_uid IN (' . implode(',', $ids) . ')');
+    }
+
+}
index 53fa4af..f31f2c5 100644 (file)
@@ -32,6 +32,10 @@ http://pear.php.net/dtd/package-2.0.xsd">
     <dir name="Horde">
      <file name="History.php" role="php" />
      <file name="HistoryObject.php" role="php" />
+     <dir name="History">
+      <file name="Factory.php" role="php" />
+      <file name="Sql.php" role="php" />
+    </dir> <!-- /lib/Horde/History -->
     </dir> <!-- /lib/Horde -->
    </dir> <!-- /lib -->
    <dir name="test">
@@ -62,6 +66,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
   <filelist>
    <install name="lib/Horde/History.php" as="Horde/History.php" />
    <install name="lib/Horde/HistoryObject.php" as="Horde/HistoryObject.php" />
+   <install name="lib/Horde/History/Factory.php" as="Horde/History/Factory.php" />
+   <install name="lib/Horde/History/Sql.php" as="Horde/History/Sql.php" />
    <install name="test/Horde/History/AllTests.php" as="Horde/History/AllTests.php" />
    <install name="test/Horde/History/InterfaceTest.php" as="Horde/History/InterfaceTest.php" />
   </filelist>
index e9491b1..b37e71d 100644 (file)
@@ -44,11 +44,9 @@ class Horde_History_InterfaceTest extends PHPUnit_Framework_TestCase
      */
     private $_db_file;
 
-
-    public function setUp()
-    {
-    }
-
+    /**
+     * Test cleanup.
+     */
     public function tearDown()
     {
         if (!empty($this->_db_file)) {
@@ -64,7 +62,10 @@ class Horde_History_InterfaceTest extends PHPUnit_Framework_TestCase
     public function getEnvironments()
     {
         if (empty($this->_environments)) {
-            /** The db environment provides our only test scenario before refactoring */
+            /**
+             * The db environment provides our only test scenario before
+             * refactoring.
+             */
             $this->_environments = array(self::ENVIRONMENT_DB);
         }
         return $this->_environments;
@@ -123,7 +124,19 @@ EOL;
                 $conf['sql']['charset'] = 'utf-8';
                 $conf['sql']['phptype'] = 'sqlite';
 
-                $history = new Horde_History();
+                $injector = new Horde_Injector(new Horde_Injector_TopLevel());
+                $injector->bindFactory(
+                    'Horde_History',
+                    'Horde_History_Factory',
+                    'getHistory'
+                );
+
+                $config = new stdClass;
+                $config->driver = 'Sql';
+                $config->params = $conf['sql'];
+                $injector->setInstance('Horde_History_Config', $config);
+
+                $history = $injector->getInstance('Horde_History');
                 break;
             }
         }
@@ -137,7 +150,9 @@ EOL;
         foreach ($this->getEnvironments() as $environment) {
             $history = $this->getHistory($environment);
             $history1 = Horde_History::singleton();
+            $this->assertType('Horde_History', $history1);
             $history2 = Horde_History::singleton();
+            $this->assertType('Horde_History', $history2);
             $this->assertSame($history1, $history2);
         }
     }
@@ -149,7 +164,7 @@ EOL;
             $history = $this->getHistory($environment);
             $history->log('test', array('action' => 'test'));
             $this->assertTrue($history->getActionTimestamp('test', 'test') > 0);
-           $data = $history->getHistory('test')->getData();
+            $data = $history->getHistory('test')->getData();
             $this->assertTrue(isset($data[0]['who']));
         }
     }
@@ -236,7 +251,7 @@ EOL;
                     'who'    => 'you',
                     'id'     => 2,
                     'ts'     => 2000,
-                   'extra'  => array('a' => 'a'),
+                    'extra'  => array('a' => 'a'),
                 ),
             );
             $this->assertEquals($expect, $data);
@@ -257,7 +272,7 @@ EOL;
             $history = $this->getHistory($environment);
             $history->log('test', array('who' => 'me', 'ts' => 1000, 'action' => 'test'));
             $history->log('test', array('who' => 'you', 'ts' => 2000, 'action' => 'yours', 'extra' => array('a' => 'a')));
-           $result = $history->getByTimestamp('>', 1, array(array('field' => 'who', 'op' => '=', 'value' => 'you')));
+            $result = $history->getByTimestamp('>', 1, array(array('field' => 'who', 'op' => '=', 'value' => 'you')));
             $this->assertEquals(array('test' => 2), $result);
         }
     }
@@ -269,7 +284,7 @@ EOL;
             $history->log('test:a', array('who' => 'me', 'ts' => 1000, 'action' => 'test'));
             $history->log('test:b', array('who' => 'you', 'ts' => 2000, 'action' => 'yours'));
             $history->log('yours', array('who' => 'you', 'ts' => 3000, 'action' => 'yours'));
-           $result = $history->getByTimestamp('>', 1, array(), 'test');
+            $result = $history->getByTimestamp('>', 1, array(), 'test');
             $this->assertEquals(array('test:a' => 1, 'test:b' => 2), $result);
         }
     }
@@ -281,7 +296,7 @@ EOL;
             $history = $this->getHistory($environment);
             $history->log('test', array('who' => 'me', 'ts' => 1000, 'action' => 'test'));
             $history->log('test', array('who' => 'you', 'ts' => 2000, 'action' => 'yours', 'extra' => array('a' => 'a')));
-           $result = $history->getByTimestamp('<', 1001);
+            $result = $history->getByTimestamp('<', 1001);
             $this->assertEquals(array('test' => 1), $result);
         }
     }