source: trunk/lib/RecordVersion.inc.php @ 136

Last change on this file since 136 was 136, checked in by scdev, 18 years ago

Q - Merged branches/2.0singleton into trunk. Completed updating classes to use singleton methods. Implemented tests. Fixed some bugs. Changed some interfaces.

File size: 17.0 KB
Line 
1<?php
2/**
3 * RecordVersion.inc.php
4 * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
5 *
6 * The RecordVersion class provides a system for saving, reviewing, and
7 * restoring versions of a record of any DB table. All the data in the record is
8 * serialized, compressed, and saved in a blob in the version_tbl. Restoring a
9 * version simply does a REPLACE INTO of the data. It is very simple, and works
10 * with multiple database tables, but the drawback is that relationships for
11 * a record cannot be retained. For example, an article from an article_tbl can
12 * be saved, but not categories associated to the record in a category_article_tbl.
13 * The restored article will simple retain the relationships that the previous
14 * current article had.
15 *
16 * @author  Quinn Comendant <quinn@strangecode.com>
17 * @version 2.1
18 */
19class RecordVersion {
20
21    // Configuration of this object.
22    var $_params = array(
23        'max_qty' => 100, // Never have more than this many versions of each record.
24        'min_qty' => 25, // Keep at least this many versions of each record.
25        'min_days' => 7, // Keep ALL versions within this many days, even if MORE than min_qty.
26        'db_table' => 'version_tbl',
27        'create_table' => true, // Automatically create table and verify columns. Better set to false after site launch.
28        'db_schema_strict' => true, // If true, makes an exact comparison of saved vs. live table schemas. If false, just checks that the saved columns are available.
29    );
30
31    // Auth_SQL object from which to access a current user_id.
32    var $_auth;
33
34    /**
35     * This method enforces the singleton pattern for this class.
36     *
37     * @return  object  Reference to the global RecordLock object.
38     * @access  public
39     * @static
40     */
41    function &getInstance($auth_object)
42    {
43        static $instance = null;
44
45        if ($instance === null) {
46            $instance = new RecordVersion($auth_object);
47        }
48
49        return $instance;
50    }
51
52    /**
53     * Constructor. Pass an Auth object on which to perform user lookups.
54     *
55     * @param mixed  $auth_object  An Auth_SQL object.
56     */
57    function RecordVersion($auth_object)
58    {
59        $app =& App::getInstance();
60
61        if (!method_exists($auth_object, 'getVal') || !method_exists($auth_object, 'getUsername')) {
62            trigger_error('Constructor not provided a valid Auth_* object.', E_USER_ERROR);
63        }
64
65        $this->_auth = $auth_object;
66
67        // Get create tables config from global context.
68        if (!is_null($app->getParam('db_create_tables'))) {
69            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
70        }
71    }
72
73    /**
74     * Setup the database table for this class.
75     *
76     * @access  public
77     * @author  Quinn Comendant <quinn@strangecode.com>
78     * @since   26 Aug 2005 17:09:36
79     */
80    function initDB($recreate_db=false)
81    {
82        $app =& App::getInstance();
83        $db =& DB::getInstance();
84
85        static $_db_tested = false;
86
87        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
88            if ($recreate_db) {
89                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
90                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_DEBUG, __FILE__, __LINE__);
91            }
92            $db->query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_table') . " (
93                version_id int NOT NULL auto_increment,
94                record_table varchar(255) NOT NULL default '',
95                record_key varchar(255) NOT NULL default '',
96                record_val varchar(255) NOT NULL default '',
97                version_data mediumblob NOT NULL,
98                version_title varchar(255) NOT NULL default '',
99                version_notes varchar(255) NOT NULL default '',
100                saved_by_admin_id smallint(11) NOT NULL default '0',
101                version_datetime datetime NOT NULL default '0000-00-00 00:00:00',
102                PRIMARY KEY (version_id),
103                KEY record_table (record_table),
104                KEY record_key (record_key),
105                KEY record_val (record_val)
106            )");
107
108            if (!$db->columnExists($this->getParam('db_table'), array(
109                'version_id',
110                'record_table',
111                'record_key',
112                'record_val',
113                'version_data',
114                'version_title',
115                'version_notes',
116                'saved_by_admin_id',
117                'version_datetime',
118            ), false, false)) {
119                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
120                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
121            }
122        }
123        $_db_tested = true;
124    }
125
126    /**
127     * Set the params of this object.
128     *
129     * @param  array $params   Array of param keys and values to set.
130     */
131    function setParam($params=null)
132    {
133        if (isset($params) && is_array($params)) {
134            // Merge new parameters with old overriding only those passed.
135            $this->_params = array_merge($this->_params, $params);
136        }
137    }
138
139    /**
140     * Return the value of a parameter, if it exists.
141     *
142     * @access public
143     * @param string $param        Which parameter to return.
144     * @return mixed               Configured parameter value.
145     */
146    function getParam($param)
147    {
148        $app =& App::getInstance();
149   
150        if (isset($this->_params[$param])) {
151            return $this->_params[$param];
152        } else {
153            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_NOTICE, __FILE__, __LINE__);
154            return null;
155        }
156    }
157
158    /**
159     * Saves a version of the current record into the version table.
160     *
161     * @param string $record_table  The table containing the record.
162     * @param string $record_key    The key column for the record.
163     * @param string $record_val    The value of the key column for the record.
164     * @param string $title         The title of this record. Only used for human presentation.
165     *
166     * @return int                  The id for the version (mysql last insert id).
167     */
168    function create($record_table, $record_key, $record_val, $title='', $notes='')
169    {
170        $app =& App::getInstance();
171        $db =& DB::getInstance();
172
173        $this->initDB();
174
175        // Get current record.
176        if (!$record = $this->getCurrent($record_table, $record_key, $record_val)) {
177            $app->logMsg(sprintf('Could not create %s version, record not found: %s, %s, %s.', $title, $record_table, $record_key, $record_val), LOG_ERR, __FILE__, __LINE__);
178            return false;
179        }
180
181        // Clean-up old versions.
182        $this->deleteOld($record_table, $record_key, $record_val);
183
184        // Save as new version.
185        $db->query("
186            INSERT INTO " . $this->getParam('db_table') . " (
187                record_table,
188                record_key,
189                record_val,
190                version_data,
191                version_title,
192                version_notes,
193                saved_by_admin_id,
194                version_datetime
195            ) VALUES (
196                '" . $db->escapeString($record_table) . "',
197                '" . $db->escapeString($record_key) . "',
198                '" . $db->escapeString($record_val) . "',
199                '" . $db->escapeString(gzcompress(serialize($record), 9)) . "',
200                '" . $db->escapeString($title) . "',
201                '" . $db->escapeString($notes) . "',
202                '" . $db->escapeString($this->_auth->getVal('user_id')) . "',
203                NOW()
204            )
205        ");
206
207        return mysql_insert_id($db->getDBH());
208    }
209
210    /**
211     * Copy a version back into it's original table.
212     *
213     * @param string $version_id    The id of the version to restore.
214     *
215     * @return int                  The id for the version (mysql last insert id).
216     */
217    function restore($version_id)
218    {
219        $app =& App::getInstance();
220        $db =& DB::getInstance();
221
222        $this->initDB();
223
224        // Get version data.
225        $qid = $db->query("
226            SELECT * FROM " . $this->getParam('db_table') . "
227            WHERE version_id = '" . $db->escapeString($version_id) . "'
228        ");
229        if (!$record = mysql_fetch_assoc($qid)) {
230            $app->raiseMsg(sprintf(_("Version ID %s%s not found."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), MSG_WARNING, __FILE__, __LINE__);
231            $app->logMsg(sprintf(_("Version ID %s%s not found."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), LOG_WARNING, __FILE__, __LINE__);
232            return false;
233        }
234        $data = unserialize(gzuncompress($record['version_data']));
235
236        // Ensure saved db columns match current table schema.
237        if (!$db->columnExists($record['record_table'], array_keys($data), $this->getParam('db_schema_strict'))) {
238            $app->raiseMsg(sprintf(_("Version ID %s%s is not compatible with the current database table."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), MSG_ERR, __FILE__, __LINE__);
239            $app->logMsg(sprintf(_("Version ID %s%s restoration failed, DB schema does not match for table %s."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')'), $record['record_table']), LOG_ALERT, __FILE__, __LINE__);
240            return false;
241        }
242
243        // SQLize the keys of the specified versioned record.
244        $replace_keys = join(",\n", array_map(array($db, 'escapeString'), array_keys($data)));
245
246        // SQLize the keys of the values of the specified versioned record. (These are more complex because we need to account for SQL null values.)
247        $replace_values = '';
248        $comma = '';
249        foreach ($data as $v) {
250            $replace_values .= is_null($v) ? "$comma\nNULL" : "$comma\n'" . $db->escapeString($v) . "'";
251            $comma = ',';
252        }
253
254        // Replace current record with specified versioned record.
255        $db->query("
256            REPLACE INTO " . $record['record_table'] . " (
257                $replace_keys
258            ) VALUES (
259                $replace_values
260            )
261        ");
262
263        return $record;
264    }
265
266    /**
267     * Version garbage collection. Deletes versions older than min_days
268     * when quantity of versions exceeds min_qty. If quantity
269     * exceeds 100 within min_days, the oldest are deleted to bring the
270     * quantity back down to min_qty.
271     *
272     * @param string $record_table  The table containing the record.
273     * @param string $record_key    The key column for the record.
274     * @param string $record_val    The value of the key column for the record.
275     *
276     * @return mixed                Array of versions, or false if none.
277     */
278    function deleteOld($record_table, $record_key, $record_val)
279    {
280        $db =& DB::getInstance();
281   
282        $this->initDB();
283
284        // Get total number of versions for this record.
285        $qid = $db->query("
286            SELECT COUNT(*) FROM " . $this->getParam('db_table') . "
287            WHERE record_table = '" . $db->escapeString($record_table) . "'
288            AND record_key = '" . $db->escapeString($record_key) . "'
289            AND record_val = '" . $db->escapeString($record_val) . "'
290        ");
291        list($v_count) = mysql_fetch_row($qid);
292
293        if ($v_count > $this->getParam('min_qty')) {
294            if ($v_count > $this->getParam('max_qty')) {
295                // To prevent a record bomb, limit max number of versions to max_qty.
296                // First query for oldest records, selecting enough to bring total number down to min_qty.
297                $qid = $db->query("
298                    SELECT version_id FROM " . $this->getParam('db_table') . "
299                    WHERE record_table = '" . $db->escapeString($record_table) . "'
300                    AND record_key = '" . $db->escapeString($record_key) . "'
301                    AND record_val = '" . $db->escapeString($record_val) . "'
302                    ORDER BY version_datetime ASC
303                    LIMIT " . ($v_count - $this->getParam('min_qty')) . "
304                ");
305                while (list($old_id) = mysql_fetch_row($qid)) {
306                    $old_versions[] = $old_id;
307                }
308                $db->query("
309                    DELETE FROM " . $this->getParam('db_table') . "
310                    WHERE version_id IN ('" . join("','", $old_versions) . "')
311                ");
312            } else {
313                // Delete versions older than min_days, while still keeping min_qty.
314                $qid = $db->query("
315                    SELECT version_id FROM " . $this->getParam('db_table') . "
316                    WHERE record_table = '" . $db->escapeString($record_table) . "'
317                    AND record_key = '" . $db->escapeString($record_key) . "'
318                    AND record_val = '" . $db->escapeString($record_val) . "'
319                    AND DATE_ADD(version_datetime, INTERVAL '" . $this->getParam('min_days') . "' DAY) < NOW()
320                    ORDER BY version_datetime ASC
321                    LIMIT " . ($v_count - $this->getParam('min_qty')) . "
322                ");
323                while (list($old_id) = mysql_fetch_row($qid)) {
324                    $old_versions[] = $old_id;
325                }
326                if (sizeof($old_versions) > 0) {
327                    $db->query("
328                        DELETE FROM " . $this->getParam('db_table') . "
329                        WHERE version_id IN ('" . join("','", $old_versions) . "')
330                    ");
331                }
332            }
333        }
334    }
335
336    /**
337     * Get a list of versions of specified record.
338     *
339     * @param string $record_table  The table containing the record.
340     * @param string $record_key    The key column for the record.
341     * @param string $record_val    The value of the key column for the record.
342     *
343     * @return mixed                Array of versions, or false if none.
344     */
345    function getList($record_table, $record_key, $record_val)
346    {
347        $db =& DB::getInstance();
348   
349        $this->initDB();
350
351        // Get versions of this record.
352        $qid = $db->query("
353            SELECT version_id, saved_by_admin_id, version_datetime, version_title
354            FROM " . $this->getParam('db_table') . "
355            WHERE record_table = '" . $db->escapeString($record_table) . "'
356            AND record_key = '" . $db->escapeString($record_key) . "'
357            AND record_val = '" . $db->escapeString($record_val) . "'
358            ORDER BY version_datetime DESC
359        ");
360        $versions = array();
361        while ($row = mysql_fetch_assoc($qid)) {
362            // Get admin usernames.
363            $row['editor'] = $this->_auth->getVal('auth_type') . ' ' . $this->_auth->getUsername($row['saved_by_admin_id']);
364            $versions[] = $row;
365        }
366        return $versions;
367    }
368
369    /**
370     * Get the version record for a specified version id.
371     *
372     * @param string $version_id    The id of the version to restore.
373     *
374     * @return mixed                Array of data saved in version, or false if none.
375     */
376    function getVerson($version_id)
377    {
378        $db =& DB::getInstance();
379   
380        $this->initDB();
381
382        // Get version data.
383        $qid = $db->query("
384            SELECT * FROM " . $this->getParam('db_table') . "
385            WHERE version_id = '" . $db->escapeString($version_id) . "'
386        ");
387        return mysql_fetch_assoc($qid);
388    }
389
390    /**
391     * Get the data stored for a specified version id.
392     *
393     * @param string $version_id    The id of the version to restore.
394     *
395     * @return mixed                Array of data saved in version, or false if none.
396     */
397    function getData($version_id)
398    {
399        $db =& DB::getInstance();
400   
401        $this->initDB();
402
403        // Get version data.
404        $qid = $db->query("
405            SELECT * FROM " . $this->getParam('db_table') . "
406            WHERE version_id = '" . $db->escapeString($version_id) . "'
407        ");
408        $record = mysql_fetch_assoc($qid);
409        if (isset($record['version_data'])) {
410            return unserialize(gzuncompress($record['version_data']));
411        } else {
412            return false;
413        }
414    }
415
416    /**
417     * Get the current record data from the original table.
418     *
419     * @param string $version_id    The id of the version to restore.
420     *
421     * @return mixed                Array of data saved in version, or false if none.
422     */
423    function getCurrent($record_table, $record_key, $record_val)
424    {
425        $db =& DB::getInstance();
426   
427        $this->initDB();
428
429        $qid = $db->query("
430            SELECT * FROM " . $db->escapeString($record_table) . "
431            WHERE " . $db->escapeString($record_key) . " = '" . $db->escapeString($record_val) . "'
432        ");
433        if ($record = mysql_fetch_assoc($qid)) {
434            return $record;
435        } else {
436            return false;
437        }
438    }
439
440
441} // End of class.
442?>
Note: See TracBrowser for help on using the repository browser.