Joomla Platform  13.1
Documentation des API du framework Joomla Platform
 Tout Classes Espaces de nommage Fichiers Fonctions Variables Pages
table.php
Aller à la documentation de ce fichier.
1 <?php
2 /**
3  * @package Joomla.Platform
4  * @subpackage Table
5  *
6  * @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved.
7  * @license GNU General Public License version 2 or later; see LICENSE
8  */
9 
10 defined('JPATH_PLATFORM') or die;
11 
12 jimport('joomla.filesystem.path');
13 
14 /**
15  * Abstract Table class
16  *
17  * Parent class to all tables.
18  *
19  * @package Joomla.Platform
20  * @subpackage Table
21  * @link http://docs.joomla.org/JTable
22  * @since 11.1
23  * @tutorial Joomla.Platform/jtable.cls
24  */
25 abstract class JTable extends JObject implements JObservableInterface
26 {
27  /**
28  * Include paths for searching for JTable classes.
29  *
30  * @var array
31  * @since 12.1
32  */
33  private static $_includePaths = array();
34 
35  /**
36  * Name of the database table to model.
37  *
38  * @var string
39  * @since 11.1
40  */
41  protected $_tbl = '';
42 
43  /**
44  * Name of the primary key field in the table.
45  *
46  * @var string
47  * @since 11.1
48  */
49  protected $_tbl_key = '';
50 
51  /**
52  * Name of the primary key fields in the table.
53  *
54  * @var array
55  * @since 12.2
56  */
57  protected $_tbl_keys = array();
58 
59  /**
60  * JDatabaseDriver object.
61  *
62  * @var JDatabaseDriver
63  * @since 11.1
64  */
65  protected $_db;
66 
67  /**
68  * Should rows be tracked as ACL assets?
69  *
70  * @var boolean
71  * @since 11.1
72  */
73  protected $_trackAssets = false;
74 
75  /**
76  * The rules associated with this record.
77  *
78  * @var JAccessRules A JAccessRules object.
79  * @since 11.1
80  */
81  protected $_rules;
82 
83  /**
84  * Indicator that the tables have been locked.
85  *
86  * @var boolean
87  * @since 11.1
88  */
89  protected $_locked = false;
90 
91  /**
92  * Indicates that the primary keys autoincrement.
93  *
94  * @var boolean
95  * @since 12.3
96  */
97  protected $_autoincrement = true;
98 
99  /**
100  * Generic observers for this JTable (Used e.g. for tags Processing)
101  *
102  * @var JObserverUpdater
103  * @since 3.1.2
104  */
105  protected $_observers;
106 
107  /**
108  * Object constructor to set table and key fields. In most cases this will
109  * be overridden by child classes to explicitly set the table and key fields
110  * for a particular database table.
111  *
112  * @param string $table Name of the table to model.
113  * @param mixed $key Name of the primary key field in the table or array of field names that compose the primary key.
114  * @param JDatabaseDriver $db JDatabaseDriver object.
115  *
116  * @since 11.1
117  */
118  public function __construct($table, $key, JDatabaseDriver $db)
119  {
120  // Set internal variables.
121  $this->_tbl = $table;
122 
123  // Set the key to be an array.
124  if (is_string($key))
125  {
126  $key = array($key);
127  }
128  elseif (is_object($key))
129  {
130  $key = (array) $key;
131  }
132 
133  $this->_tbl_keys = $key;
134 
135  if (count($key) == 1)
136  {
137  $this->_autoincrement = true;
138  }
139  else
140  {
141  $this->_autoincrement = false;
142  }
143 
144  // Set the singular table key for backwards compatibility.
145  $this->_tbl_key = $this->getKeyName();
146 
147  $this->_db = $db;
148 
149  // Initialise the table properties.
150  $fields = $this->getFields();
151 
152  if ($fields)
153  {
154  foreach ($fields as $name => $v)
155  {
156  // Add the field if it is not already present.
157  if (!property_exists($this, $name))
158  {
159  $this->$name = null;
160  }
161  }
162  }
163 
164  // If we are tracking assets, make sure an access field exists and initially set the default.
165  if (property_exists($this, 'asset_id'))
166  {
167  $this->_trackAssets = true;
168  }
169 
170  // If the access property exists, set the default.
171  if (property_exists($this, 'access'))
172  {
173  $this->access = (int) JFactory::getConfig()->get('access');
174  }
175 
176  // Implement JObservableInterface:
177  // Create observer updater and attaches all observers interested by $this class:
178  $this->_observers = new JObserverUpdater($this);
179  JObserverMapper::attachAllObservers($this);
180  }
181 
182  /**
183  * Implement JObservableInterface:
184  * Adds an observer to this instance.
185  * This method will be called fron the constructor of classes implementing JObserverInterface
186  * which is instanciated by the constructor of $this with JObserverMapper::attachAllObservers($this)
187  *
188  * @param JObserverInterface|JTableObserver $observer The observer object
189  *
190  * @return void
191  *
192  * @since 3.1.2
193  */
194  public function attachObserver(JObserverInterface $observer)
195  {
196  $this->_observers->attachObserver($observer);
197  }
198 
199  /**
200  * Gets the instance of the observer of class $observerClass
201  *
202  * @param string $observerClass The observer class-name to return the object of
203  *
204  * @return JTableObserver|null
205  *
206  * @since 3.1.2
207  */
208  public function getObserverOfClass($observerClass)
209  {
210  return $this->_observers->getObserverOfClass($observerClass);
211  }
212 
213  /**
214  * Get the columns from database table.
215  *
216  * @return mixed An array of the field names, or false if an error occurs.
217  *
218  * @since 11.1
219  * @throws UnexpectedValueException
220  */
221  public function getFields()
222  {
223  static $cache = null;
224 
225  if ($cache === null)
226  {
227  // Lookup the fields for this table only once.
228  $name = $this->_tbl;
229  $fields = $this->_db->getTableColumns($name, false);
230 
231  if (empty($fields))
232  {
233  throw new UnexpectedValueException(sprintf('No columns found for %s table', $name));
234  }
235 
236  $cache = $fields;
237  }
238 
239  return $cache;
240  }
241 
242  /**
243  * Static method to get an instance of a JTable class if it can be found in
244  * the table include paths. To add include paths for searching for JTable
245  * classes see JTable::addIncludePath().
246  *
247  * @param string $type The type (name) of the JTable class to get an instance of.
248  * @param string $prefix An optional prefix for the table class name.
249  * @param array $config An optional array of configuration values for the JTable object.
250  *
251  * @return mixed A JTable object if found or boolean false if one could not be found.
252  *
253  * @link http://docs.joomla.org/JTable/getInstance
254  * @since 11.1
255  */
256  public static function getInstance($type, $prefix = 'JTable', $config = array())
257  {
258  // Sanitize and prepare the table class name.
259  $type = preg_replace('/[^A-Z0-9_\.-]/i', '', $type);
260  $tableClass = $prefix . ucfirst($type);
261 
262  // Only try to load the class if it doesn't already exist.
263  if (!class_exists($tableClass))
264  {
265  // Search for the class file in the JTable include paths.
266  $path = JPath::find(self::addIncludePath(), strtolower($type) . '.php');
267 
268  if ($path)
269  {
270  // Import the class file.
271  include_once $path;
272 
273  // If we were unable to load the proper class, raise a warning and return false.
274  if (!class_exists($tableClass))
275  {
276  JLog::add(JText::sprintf('JLIB_DATABASE_ERROR_CLASS_NOT_FOUND_IN_FILE', $tableClass), JLog::WARNING, 'jerror');
277 
278  return false;
279  }
280  }
281  else
282  {
283  // If we were unable to find the class file in the JTable include paths, raise a warning and return false.
284  JLog::add(JText::sprintf('JLIB_DATABASE_ERROR_NOT_SUPPORTED_FILE_NOT_FOUND', $type), JLog::WARNING, 'jerror');
285 
286  return false;
287  }
288  }
289 
290  // If a database object was passed in the configuration array use it, otherwise get the global one from JFactory.
291  $db = isset($config['dbo']) ? $config['dbo'] : JFactory::getDbo();
292 
293  // Instantiate a new table class and return it.
294  return new $tableClass($db);
295  }
296 
297  /**
298  * Add a filesystem path where JTable should search for table class files.
299  * You may either pass a string or an array of paths.
300  *
301  * @param mixed $path A filesystem path or array of filesystem paths to add.
302  *
303  * @return array An array of filesystem paths to find JTable classes in.
304  *
305  * @link http://docs.joomla.org/JTable/addIncludePath
306  * @since 11.1
307  */
308  public static function addIncludePath($path = null)
309  {
310  // If the internal paths have not been initialised, do so with the base table path.
311  if (empty(self::$_includePaths))
312  {
313  self::$_includePaths = array(__DIR__);
314  }
315 
316  // Convert the passed path(s) to add to an array.
317  settype($path, 'array');
318 
319  // If we have new paths to add, do so.
320  if (!empty($path))
321  {
322  // Check and add each individual new path.
323  foreach ($path as $dir)
324  {
325  // Sanitize path.
326  $dir = trim($dir);
327 
328  // Add to the front of the list so that custom paths are searched first.
329  if (!in_array($dir, self::$_includePaths))
330  {
331  array_unshift(self::$_includePaths, $dir);
332  }
333  }
334  }
335 
336  return self::$_includePaths;
337  }
338 
339  /**
340  * Method to compute the default name of the asset.
341  * The default name is in the form table_name.id
342  * where id is the value of the primary key of the table.
343  *
344  * @return string
345  *
346  * @since 11.1
347  */
348  protected function _getAssetName()
349  {
350  $keys = array();
351 
352  foreach ($this->_tbl_keys as $k)
353  {
354  $keys[] = (int) $this->$k;
355  }
356 
357  return $this->_tbl . '.' . implode('.', $keys);
358  }
359 
360  /**
361  * Method to return the title to use for the asset table. In
362  * tracking the assets a title is kept for each asset so that there is some
363  * context available in a unified access manager. Usually this would just
364  * return $this->title or $this->name or whatever is being used for the
365  * primary name of the row. If this method is not overridden, the asset name is used.
366  *
367  * @return string The string to use as the title in the asset table.
368  *
369  * @link http://docs.joomla.org/JTable/getAssetTitle
370  * @since 11.1
371  */
372  protected function _getAssetTitle()
373  {
374  return $this->_getAssetName();
375  }
376 
377  /**
378  * Method to get the parent asset under which to register this one.
379  * By default, all assets are registered to the ROOT node with ID,
380  * which will default to 1 if none exists.
381  * The extended class can define a table and id to lookup. If the
382  * asset does not exist it will be created.
383  *
384  * @param JTable $table A JTable object for the asset parent.
385  * @param integer $id Id to look up
386  *
387  * @return integer
388  *
389  * @since 11.1
390  */
391  protected function _getAssetParentId(JTable $table = null, $id = null)
392  {
393  // For simple cases, parent to the asset root.
394  $assets = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo()));
395  $rootId = $assets->getRootId();
396 
397  if (!empty($rootId))
398  {
399  return $rootId;
400  }
401 
402  return 1;
403  }
404 
405  /**
406  * Method to append the primary keys for this table to a query.
407  *
408  * @param JDatabaseQuery $query A query object to append.
409  * @param mixed $pk Optional primary key parameter.
410  *
411  * @return void
412  *
413  * @since 12.3
414  */
415  public function appendPrimaryKeys($query, $pk = null)
416  {
417  if (is_null($pk))
418  {
419  foreach ($this->_tbl_keys as $k)
420  {
421  $query->where($this->_db->quoteName($k) . ' = ' . $this->_db->quote($this->$k));
422  }
423  }
424  else
425  {
426  if (is_string($pk))
427  {
428  $pk = array($this->_tbl_key => $pk);
429  }
430 
431  $pk = (object) $pk;
432 
433  foreach ($this->_tbl_keys AS $k)
434  {
435  $query->where($this->_db->quoteName($k) . ' = ' . $this->_db->quote($pk->$k));
436  }
437  }
438  }
439 
440  /**
441  * Method to get the database table name for the class.
442  *
443  * @return string The name of the database table being modeled.
444  *
445  * @since 11.1
446  *
447  * @link http://docs.joomla.org/JTable/getTableName
448  */
449  public function getTableName()
450  {
451  return $this->_tbl;
452  }
453 
454  /**
455  * Method to get the primary key field name for the table.
456  *
457  * @param boolean $multiple True to return all primary keys (as an array) or false to return just the first one (as a string).
458  *
459  * @return mixed Array of primary key field names or string containing the first primary key field.
460  *
461  * @link http://docs.joomla.org/JTable/getKeyName
462  * @since 11.1
463  */
464  public function getKeyName($multiple = false)
465  {
466  // Count the number of keys
467  if (count($this->_tbl_keys))
468  {
469  if ($multiple)
470  {
471  // If we want multiple keys, return the raw array.
472  return $this->_tbl_keys;
473  }
474  else
475  {
476  // If we want the standard method, just return the first key.
477  return $this->_tbl_keys[0];
478  }
479  }
480 
481  return '';
482  }
483 
484  /**
485  * Method to get the JDatabaseDriver object.
486  *
487  * @return JDatabaseDriver The internal database driver object.
488  *
489  * @link http://docs.joomla.org/JTable/getDBO
490  * @since 11.1
491  */
492  public function getDbo()
493  {
494  return $this->_db;
495  }
496 
497  /**
498  * Method to set the JDatabaseDriver object.
499  *
500  * @param JDatabaseDriver $db A JDatabaseDriver object to be used by the table object.
501  *
502  * @return boolean True on success.
503  *
504  * @link http://docs.joomla.org/JTable/setDBO
505  * @since 11.1
506  */
507  public function setDBO(JDatabaseDriver $db)
508  {
509  $this->_db = $db;
510 
511  return true;
512  }
513 
514  /**
515  * Method to set rules for the record.
516  *
517  * @param mixed $input A JAccessRules object, JSON string, or array.
518  *
519  * @return void
520  *
521  * @since 11.1
522  */
523  public function setRules($input)
524  {
525  if ($input instanceof JAccessRules)
526  {
527  $this->_rules = $input;
528  }
529  else
530  {
531  $this->_rules = new JAccessRules($input);
532  }
533  }
534 
535  /**
536  * Method to get the rules for the record.
537  *
538  * @return JAccessRules object
539  *
540  * @since 11.1
541  */
542  public function getRules()
543  {
544  return $this->_rules;
545  }
546 
547  /**
548  * Method to reset class properties to the defaults set in the class
549  * definition. It will ignore the primary key as well as any private class
550  * properties (except $_errors).
551  *
552  * @return void
553  *
554  * @link http://docs.joomla.org/JTable/reset
555  * @since 11.1
556  */
557  public function reset()
558  {
559  // Get the default values for the class from the table.
560  foreach ($this->getFields() as $k => $v)
561  {
562  // If the property is not the primary key or private, reset it.
563  if (!in_array($k, $this->_tbl_keys) && (strpos($k, '_') !== 0))
564  {
565  $this->$k = $v->Default;
566  }
567  }
568 
569  // Reset table errors
570  $this->_errors = array();
571  }
572 
573  /**
574  * Method to bind an associative array or object to the JTable instance.This
575  * method only binds properties that are publicly accessible and optionally
576  * takes an array of properties to ignore when binding.
577  *
578  * @param mixed $src An associative array or object to bind to the JTable instance.
579  * @param mixed $ignore An optional array or space separated list of properties to ignore while binding.
580  *
581  * @return boolean True on success.
582  *
583  * @link http://docs.joomla.org/JTable/bind
584  * @since 11.1
585  * @throws InvalidArgumentException
586  */
587  public function bind($src, $ignore = array())
588  {
589  // If the source value is not an array or object return false.
590  if (!is_object($src) && !is_array($src))
591  {
592  throw new InvalidArgumentException(sprintf('%s::bind(*%s*)', get_class($this), gettype($src)));
593  }
594 
595  // If the source value is an object, get its accessible properties.
596  if (is_object($src))
597  {
598  $src = get_object_vars($src);
599  }
600 
601  // If the ignore value is a string, explode it over spaces.
602  if (!is_array($ignore))
603  {
604  $ignore = explode(' ', $ignore);
605  }
606 
607  // Bind the source value, excluding the ignored fields.
608  foreach ($this->getProperties() as $k => $v)
609  {
610  // Only process fields not in the ignore array.
611  if (!in_array($k, $ignore))
612  {
613  if (isset($src[$k]))
614  {
615  $this->$k = $src[$k];
616  }
617  }
618  }
619 
620  return true;
621  }
622 
623  /**
624  * Method to load a row from the database by primary key and bind the fields
625  * to the JTable instance properties.
626  *
627  * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. If not
628  * set the instance property value is used.
629  * @param boolean $reset True to reset the default values before loading the new row.
630  *
631  * @return boolean True if successful. False if row not found.
632  *
633  * @link http://docs.joomla.org/JTable/load
634  * @since 11.1
635  * @throws InvalidArgumentException
636  * @throws RuntimeException
637  * @throws UnexpectedValueException
638  */
639  public function load($keys = null, $reset = true)
640  {
641  // Implement JObservableInterface: Pre-processing by observers
642  $this->_observers->update('onBeforeLoad', array($keys, $reset));
643 
644  if (empty($keys))
645  {
646  $empty = true;
647  $keys = array();
648 
649  // If empty, use the value of the current key
650  foreach ($this->_tbl_keys as $key)
651  {
652  $empty = $empty && empty($this->$key);
653  $keys[$key] = $this->$key;
654  }
655 
656  // If empty primary key there's is no need to load anything
657  if ($empty)
658  {
659  return true;
660  }
661  }
662  elseif (!is_array($keys))
663  {
664  // Load by primary key.
665  $keyCount = count($this->_tbl_keys);
666 
667  if ($keyCount)
668  {
669  if ($keyCount > 1)
670  {
671  throw new InvalidArgumentException('Table has multiple primary keys specified, only one primary key value provided.');
672  }
673  $keys = array($this->getKeyName() => $keys);
674  }
675  else
676  {
677  throw new RuntimeException('No table keys defined.');
678  }
679  }
680 
681  if ($reset)
682  {
683  $this->reset();
684  }
685 
686  // Initialise the query.
687  $query = $this->_db->getQuery(true)
688  ->select('*')
689  ->from($this->_tbl);
690  $fields = array_keys($this->getProperties());
691 
692  foreach ($keys as $field => $value)
693  {
694  // Check that $field is in the table.
695  if (!in_array($field, $fields))
696  {
697  throw new UnexpectedValueException(sprintf('Missing field in database: %s &#160; %s.', get_class($this), $field));
698  }
699  // Add the search tuple to the query.
700  $query->where($this->_db->quoteName($field) . ' = ' . $this->_db->quote($value));
701  }
702 
703  $this->_db->setQuery($query);
704 
705  $row = $this->_db->loadAssoc();
706 
707  // Check that we have a result.
708  if (empty($row))
709  {
710  $result = false;
711  }
712  else
713  {
714  // Bind the object with the row and return.
715  $result = $this->bind($row);
716  }
717 
718  // Implement JObservableInterface: Post-processing by observers
719  $this->_observers->update('onAfterLoad', array(&$result, $row));
720 
721  return $result;
722  }
723 
724  /**
725  * Method to perform sanity checks on the JTable instance properties to ensure
726  * they are safe to store in the database. Child classes should override this
727  * method to make sure the data they are storing in the database is safe and
728  * as expected before storage.
729  *
730  * @return boolean True if the instance is sane and able to be stored in the database.
731  *
732  * @link http://docs.joomla.org/JTable/check
733  * @since 11.1
734  */
735  public function check()
736  {
737  return true;
738  }
739 
740  /**
741  * Method to store a row in the database from the JTable instance properties.
742  * If a primary key value is set the row with that primary key value will be
743  * updated with the instance property values. If no primary key value is set
744  * a new row will be inserted into the database with the properties from the
745  * JTable instance.
746  *
747  * @param boolean $updateNulls True to update fields even if they are null.
748  *
749  * @return boolean True on success.
750  *
751  * @link http://docs.joomla.org/JTable/store
752  * @since 11.1
753  */
754  public function store($updateNulls = false)
755  {
756  $k = $this->_tbl_keys;
757 
758  // Implement JObservableInterface: Pre-processing by observers
759  $this->_observers->update('onBeforeStore', array($updateNulls, $k));
760 
761  $currentAssetId = 0;
762 
763  if (!empty($this->asset_id))
764  {
765  $currentAssetId = $this->asset_id;
766  }
767 
768  // The asset id field is managed privately by this class.
769  if ($this->_trackAssets)
770  {
771  unset($this->asset_id);
772  }
773 
774  // If a primary key exists update the object, otherwise insert it.
775  if ($this->hasPrimaryKey())
776  {
777  $result = $this->_db->updateObject($this->_tbl, $this, $this->_tbl_keys, $updateNulls);
778  }
779  else
780  {
781  $result = $this->_db->insertObject($this->_tbl, $this, $this->_tbl_keys[0]);
782  }
783 
784  // If the table is not set to track assets return true.
785  if ($this->_trackAssets)
786  {
787  if ($this->_locked)
788  {
789  $this->_unlock();
790  }
791 
792  /*
793  * Asset Tracking
794  */
795  $parentId = $this->_getAssetParentId();
796  $name = $this->_getAssetName();
797  $title = $this->_getAssetTitle();
798 
799  $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo()));
800  $asset->loadByName($name);
801 
802  // Re-inject the asset id.
803  $this->asset_id = $asset->id;
804 
805  // Check for an error.
806  $error = $asset->getError();
807 
808  if ($error)
809  {
810  $this->setError($error);
811 
812  return false;
813  }
814  else
815  {
816  // Specify how a new or moved node asset is inserted into the tree.
817  if (empty($this->asset_id) || $asset->parent_id != $parentId)
818  {
819  $asset->setLocation($parentId, 'last-child');
820  }
821 
822  // Prepare the asset to be stored.
823  $asset->parent_id = $parentId;
824  $asset->name = $name;
825  $asset->title = $title;
826 
827  if ($this->_rules instanceof JAccessRules)
828  {
829  $asset->rules = (string) $this->_rules;
830  }
831 
832  if (!$asset->check() || !$asset->store($updateNulls))
833  {
834  $this->setError($asset->getError());
835 
836  return false;
837  }
838  else
839  {
840  // Create an asset_id or heal one that is corrupted.
841  if (empty($this->asset_id) || ($currentAssetId != $this->asset_id && !empty($this->asset_id)))
842  {
843  // Update the asset_id field in this table.
844  $this->asset_id = (int) $asset->id;
845 
846  $query = $this->_db->getQuery(true)
847  ->update($this->_db->quoteName($this->_tbl))
848  ->set('asset_id = ' . (int) $this->asset_id);
849  $this->appendPrimaryKeys($query);
850  $this->_db->setQuery($query)->execute();
851  }
852  }
853  }
854  }
855 
856  // Implement JObservableInterface: Post-processing by observers
857  $this->_observers->update('onAfterStore', array(&$result));
858 
859  return $result;
860  }
861 
862  /**
863  * Method to provide a shortcut to binding, checking and storing a JTable
864  * instance to the database table. The method will check a row in once the
865  * data has been stored and if an ordering filter is present will attempt to
866  * reorder the table rows based on the filter. The ordering filter is an instance
867  * property name. The rows that will be reordered are those whose value matches
868  * the JTable instance for the property specified.
869  *
870  * @param mixed $src An associative array or object to bind to the JTable instance.
871  * @param string $orderingFilter Filter for the order updating
872  * @param mixed $ignore An optional array or space separated list of properties
873  * to ignore while binding.
874  *
875  * @return boolean True on success.
876  *
877  * @link http://docs.joomla.org/JTable/save
878  * @since 11.1
879  */
880  public function save($src, $orderingFilter = '', $ignore = '')
881  {
882  // Attempt to bind the source to the instance.
883  if (!$this->bind($src, $ignore))
884  {
885  return false;
886  }
887 
888  // Run any sanity checks on the instance and verify that it is ready for storage.
889  if (!$this->check())
890  {
891  return false;
892  }
893 
894  // Attempt to store the properties to the database table.
895  if (!$this->store())
896  {
897  return false;
898  }
899 
900  // Attempt to check the row in, just in case it was checked out.
901  if (!$this->checkin())
902  {
903  return false;
904  }
905 
906  // If an ordering filter is set, attempt reorder the rows in the table based on the filter and value.
907  if ($orderingFilter)
908  {
909  $filterValue = $this->$orderingFilter;
910  $this->reorder($orderingFilter ? $this->_db->quoteName($orderingFilter) . ' = ' . $this->_db->quote($filterValue) : '');
911  }
912 
913  // Set the error to empty and return true.
914  $this->setError('');
915 
916  return true;
917  }
918 
919  /**
920  * Method to delete a row from the database table by primary key value.
921  *
922  * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used.
923  *
924  * @return boolean True on success.
925  *
926  * @link http://docs.joomla.org/JTable/delete
927  * @since 11.1
928  * @throws UnexpectedValueException
929  */
930  public function delete($pk = null)
931  {
932  if (is_null($pk))
933  {
934  $pk = array();
935 
936  foreach ($this->_tbl_keys AS $key)
937  {
938  $pk[$key] = $this->$key;
939  }
940  }
941  elseif (!is_array($pk))
942  {
943  $pk = array($this->_tbl_key => $pk);
944  }
945 
946  foreach ($this->_tbl_keys AS $key)
947  {
948  $pk[$key] = is_null($pk[$key]) ? $this->$key : $pk[$key];
949 
950  if ($pk[$key] === null)
951  {
952  throw new UnexpectedValueException('Null primary key not allowed.');
953  }
954  $this->$key = $pk[$key];
955  }
956 
957  // Implement JObservableInterface: Pre-processing by observers
958  $this->_observers->update('onBeforeDelete', array($pk));
959 
960  // If tracking assets, remove the asset first.
961  if ($this->_trackAssets)
962  {
963  // Get the asset name
964  $name = $this->_getAssetName();
965  $asset = self::getInstance('Asset');
966 
967  if ($asset->loadByName($name))
968  {
969  if (!$asset->delete())
970  {
971  $this->setError($asset->getError());
972 
973  return false;
974  }
975  }
976  else
977  {
978  $this->setError($asset->getError());
979 
980  return false;
981  }
982  }
983 
984  // Delete the row by primary key.
985  $query = $this->_db->getQuery(true)
986  ->delete($this->_tbl);
987  $this->appendPrimaryKeys($query, $pk);
988 
989  $this->_db->setQuery($query);
990 
991  // Check for a database error.
992  $this->_db->execute();
993 
994  // Implement JObservableInterface: Post-processing by observers
995  $this->_observers->update('onAfterDelete', array($pk));
996 
997  return true;
998  }
999 
1000  /**
1001  * Method to check a row out if the necessary properties/fields exist. To
1002  * prevent race conditions while editing rows in a database, a row can be
1003  * checked out if the fields 'checked_out' and 'checked_out_time' are available.
1004  * While a row is checked out, any attempt to store the row by a user other
1005  * than the one who checked the row out should be held until the row is checked
1006  * in again.
1007  *
1008  * @param integer $userId The Id of the user checking out the row.
1009  * @param mixed $pk An optional primary key value to check out. If not set
1010  * the instance property value is used.
1011  *
1012  * @return boolean True on success.
1013  *
1014  * @link http://docs.joomla.org/JTable/checkOut
1015  * @since 11.1
1016  * @throws UnexpectedValueException
1017  */
1018  public function checkOut($userId, $pk = null)
1019  {
1020  // If there is no checked_out or checked_out_time field, just return true.
1021  if (!property_exists($this, 'checked_out') || !property_exists($this, 'checked_out_time'))
1022  {
1023  return true;
1024  }
1025 
1026  if (is_null($pk))
1027  {
1028  $pk = array();
1029 
1030  foreach ($this->_tbl_keys AS $key)
1031  {
1032  $pk[$key] = $this->$key;
1033  }
1034  }
1035  elseif (!is_array($pk))
1036  {
1037  $pk = array($this->_tbl_key => $pk);
1038  }
1039 
1040  foreach ($this->_tbl_keys AS $key)
1041  {
1042  $pk[$key] = is_null($pk[$key]) ? $this->$key : $pk[$key];
1043 
1044  if ($pk[$key] === null)
1045  {
1046  throw new UnexpectedValueException('Null primary key not allowed.');
1047  }
1048  }
1049 
1050  // Get the current time in the database format.
1051  $time = JFactory::getDate()->toSql();
1052 
1053  // Check the row out by primary key.
1054  $query = $this->_db->getQuery(true)
1055  ->update($this->_tbl)
1056  ->set($this->_db->quoteName('checked_out') . ' = ' . (int) $userId)
1057  ->set($this->_db->quoteName('checked_out_time') . ' = ' . $this->_db->quote($time));
1058  $this->appendPrimaryKeys($query, $pk);
1059  $this->_db->setQuery($query);
1060  $this->_db->execute();
1061 
1062  // Set table values in the object.
1063  $this->checked_out = (int) $userId;
1064  $this->checked_out_time = $time;
1065 
1066  return true;
1067  }
1068 
1069  /**
1070  * Method to check a row in if the necessary properties/fields exist. Checking
1071  * a row in will allow other users the ability to edit the row.
1072  *
1073  * @param mixed $pk An optional primary key value to check out. If not set the instance property value is used.
1074  *
1075  * @return boolean True on success.
1076  *
1077  * @link http://docs.joomla.org/JTable/checkIn
1078  * @since 11.1
1079  * @throws UnexpectedValueException
1080  */
1081  public function checkIn($pk = null)
1082  {
1083  // If there is no checked_out or checked_out_time field, just return true.
1084  if (!property_exists($this, 'checked_out') || !property_exists($this, 'checked_out_time'))
1085  {
1086  return true;
1087  }
1088 
1089  if (is_null($pk))
1090  {
1091  $pk = array();
1092 
1093  foreach ($this->_tbl_keys AS $key)
1094  {
1095  $pk[$this->$key] = $this->$key;
1096  }
1097  }
1098  elseif (!is_array($pk))
1099  {
1100  $pk = array($this->_tbl_key => $pk);
1101  }
1102 
1103  foreach ($this->_tbl_keys AS $key)
1104  {
1105  $pk[$key] = empty($pk[$key]) ? $this->$key : $pk[$key];
1106 
1107  if ($pk[$key] === null)
1108  {
1109  throw new UnexpectedValueException('Null primary key not allowed.');
1110  }
1111  }
1112 
1113  // Check the row in by primary key.
1114  $query = $this->_db->getQuery(true)
1115  ->update($this->_tbl)
1116  ->set($this->_db->quoteName('checked_out') . ' = 0')
1117  ->set($this->_db->quoteName('checked_out_time') . ' = ' . $this->_db->quote($this->_db->getNullDate()));
1118  $this->appendPrimaryKeys($query, $pk);
1119  $this->_db->setQuery($query);
1120 
1121  // Check for a database error.
1122  $this->_db->execute();
1123 
1124  // Set table values in the object.
1125  $this->checked_out = 0;
1126  $this->checked_out_time = '';
1127 
1128  return true;
1129  }
1130 
1131  /**
1132  * Validate that the primary key has been set.
1133  *
1134  * @return boolean True if the primary key(s) have been set.
1135  *
1136  * @since 12.3
1137  */
1138  public function hasPrimaryKey()
1139  {
1140  if ($this->_autoincrement)
1141  {
1142  $empty = true;
1143 
1144  foreach ($this->_tbl_keys as $key)
1145  {
1146  $empty = $empty && empty($this->$key);
1147  }
1148  }
1149  else
1150  {
1151  $query = $this->_db->getQuery(true)
1152  ->select('COUNT(*)')
1153  ->from($this->_tbl);
1154  $this->appendPrimaryKeys($query);
1155 
1156  $this->_db->setQuery($query);
1157  $count = $this->_db->loadResult();
1158 
1159  if ($count == 1)
1160  {
1161  $empty = false;
1162  }
1163  else
1164  {
1165  $empty = true;
1166  }
1167  }
1168 
1169  return !$empty;
1170  }
1171 
1172  /**
1173  * Method to increment the hits for a row if the necessary property/field exists.
1174  *
1175  * @param mixed $pk An optional primary key value to increment. If not set the instance property value is used.
1176  *
1177  * @return boolean True on success.
1178  *
1179  * @link http://docs.joomla.org/JTable/hit
1180  * @since 11.1
1181  * @throws UnexpectedValueException
1182  */
1183  public function hit($pk = null)
1184  {
1185  // If there is no hits field, just return true.
1186  if (!property_exists($this, 'hits'))
1187  {
1188  return true;
1189  }
1190 
1191  if (is_null($pk))
1192  {
1193  $pk = array();
1194 
1195  foreach ($this->_tbl_keys AS $key)
1196  {
1197  $pk[$key] = $this->$key;
1198  }
1199  }
1200  elseif (!is_array($pk))
1201  {
1202  $pk = array($this->_tbl_key => $pk);
1203  }
1204 
1205  foreach ($this->_tbl_keys AS $key)
1206  {
1207  $pk[$key] = is_null($pk[$key]) ? $this->$key : $pk[$key];
1208 
1209  if ($pk[$key] === null)
1210  {
1211  throw new UnexpectedValueException('Null primary key not allowed.');
1212  }
1213  }
1214 
1215  // Check the row in by primary key.
1216  $query = $this->_db->getQuery(true)
1217  ->update($this->_tbl)
1218  ->set($this->_db->quoteName('hits') . ' = (' . $this->_db->quoteName('hits') . ' + 1)');
1219  $this->appendPrimaryKeys($query, $pk);
1220  $this->_db->setQuery($query);
1221  $this->_db->execute();
1222 
1223  // Set table values in the object.
1224  $this->hits++;
1225 
1226  return true;
1227  }
1228 
1229  /**
1230  * Method to determine if a row is checked out and therefore uneditable by
1231  * a user. If the row is checked out by the same user, then it is considered
1232  * not checked out -- as the user can still edit it.
1233  *
1234  * @param integer $with The userid to preform the match with, if an item is checked
1235  * out by this user the function will return false.
1236  * @param integer $against The userid to perform the match against when the function
1237  * is used as a static function.
1238  *
1239  * @return boolean True if checked out.
1240  *
1241  * @link http://docs.joomla.org/JTable/isCheckedOut
1242  * @since 11.1
1243  */
1244  public function isCheckedOut($with = 0, $against = null)
1245  {
1246  // Handle the non-static case.
1247  if (isset($this) && ($this instanceof JTable) && is_null($against))
1248  {
1249  $against = $this->get('checked_out');
1250  }
1251 
1252  // The item is not checked out or is checked out by the same user.
1253  if (!$against || ($against == $with))
1254  {
1255  return false;
1256  }
1257 
1258  $db = JFactory::getDbo();
1259  $db->setQuery('SELECT COUNT(userid) FROM ' . $db->quoteName('#__session') . ' WHERE ' . $db->quoteName('userid') . ' = ' . (int) $against);
1260  $checkedOut = (boolean) $db->loadResult();
1261 
1262  // If a session exists for the user then it is checked out.
1263  return $checkedOut;
1264  }
1265 
1266  /**
1267  * Method to get the next ordering value for a group of rows defined by an SQL WHERE clause.
1268  * This is useful for placing a new item last in a group of items in the table.
1269  *
1270  * @param string $where WHERE clause to use for selecting the MAX(ordering) for the table.
1271  *
1272  * @return mixed Boolean false an failure or the next ordering value as an integer.
1273  *
1274  * @link http://docs.joomla.org/JTable/getNextOrder
1275  * @since 11.1
1276  * @throws UnexpectedValueException
1277  */
1278  public function getNextOrder($where = '')
1279  {
1280  // If there is no ordering field set an error and return false.
1281  if (!property_exists($this, 'ordering'))
1282  {
1283  throw new UnexpectedValueException(sprintf('%s does not support ordering.', get_class($this)));
1284  }
1285 
1286  // Get the largest ordering value for a given where clause.
1287  $query = $this->_db->getQuery(true)
1288  ->select('MAX(ordering)')
1289  ->from($this->_tbl);
1290 
1291  if ($where)
1292  {
1293  $query->where($where);
1294  }
1295 
1296  $this->_db->setQuery($query);
1297  $max = (int) $this->_db->loadResult();
1298 
1299  // Return the largest ordering value + 1.
1300  return ($max + 1);
1301  }
1302 
1303  /**
1304  * Get the primary key values for this table using passed in values as a default.
1305  *
1306  * @param array $keys Optional primary key values to use.
1307  *
1308  * @return array An array of primary key names and values.
1309  *
1310  * @since 12.3
1311  */
1312  public function getPrimaryKey(array $keys = array())
1313  {
1314  foreach ($this->_tbl_keys as $key)
1315  {
1316  if (!isset($keys[$key]))
1317  {
1318  if (!empty($this->$key))
1319  {
1320  $keys[$key] = $this->$key;
1321  }
1322  }
1323  }
1324 
1325  return $keys;
1326  }
1327 
1328  /**
1329  * Method to compact the ordering values of rows in a group of rows
1330  * defined by an SQL WHERE clause.
1331  *
1332  * @param string $where WHERE clause to use for limiting the selection of rows to compact the ordering values.
1333  *
1334  * @return mixed Boolean True on success.
1335  *
1336  * @link http://docs.joomla.org/JTable/reorder
1337  * @since 11.1
1338  * @throws UnexpectedValueException
1339  */
1340  public function reorder($where = '')
1341  {
1342  // If there is no ordering field set an error and return false.
1343  if (!property_exists($this, 'ordering'))
1344  {
1345  throw new UnexpectedValueException(sprintf('%s does not support ordering.', get_class($this)));
1346  }
1347 
1348  $k = $this->_tbl_key;
1349 
1350  // Get the primary keys and ordering values for the selection.
1351  $query = $this->_db->getQuery(true)
1352  ->select(implode(',', $this->_tbl_keys) . ', ordering')
1353  ->from($this->_tbl)
1354  ->where('ordering >= 0')
1355  ->order('ordering');
1356 
1357  // Setup the extra where and ordering clause data.
1358  if ($where)
1359  {
1360  $query->where($where);
1361  }
1362 
1363  $this->_db->setQuery($query);
1364  $rows = $this->_db->loadObjectList();
1365 
1366  // Compact the ordering values.
1367  foreach ($rows as $i => $row)
1368  {
1369  // Make sure the ordering is a positive integer.
1370  if ($row->ordering >= 0)
1371  {
1372  // Only update rows that are necessary.
1373  if ($row->ordering != $i + 1)
1374  {
1375  // Update the row ordering field.
1376  $query->clear()
1377  ->update($this->_tbl)
1378  ->set('ordering = ' . ($i + 1));
1379  $this->appendPrimaryKeys($query, $row);
1380  $this->_db->setQuery($query);
1381  $this->_db->execute();
1382  }
1383  }
1384  }
1385 
1386  return true;
1387  }
1388 
1389  /**
1390  * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
1391  * Negative numbers move the row up in the sequence and positive numbers move it down.
1392  *
1393  * @param integer $delta The direction and magnitude to move the row in the ordering sequence.
1394  * @param string $where WHERE clause to use for limiting the selection of rows to compact the
1395  * ordering values.
1396  *
1397  * @return mixed Boolean True on success.
1398  *
1399  * @link http://docs.joomla.org/JTable/move
1400  * @since 11.1
1401  * @throws UnexpectedValueException
1402  */
1403  public function move($delta, $where = '')
1404  {
1405  // If there is no ordering field set an error and return false.
1406  if (!property_exists($this, 'ordering'))
1407  {
1408  throw new UnexpectedValueException(sprintf('%s does not support ordering.', get_class($this)));
1409  }
1410 
1411  // If the change is none, do nothing.
1412  if (empty($delta))
1413  {
1414  return true;
1415  }
1416 
1417  $k = $this->_tbl_key;
1418  $row = null;
1419  $query = $this->_db->getQuery(true);
1420 
1421  // Select the primary key and ordering values from the table.
1422  $query->select(implode(',', $this->_tbl_keys) . ', ordering')
1423  ->from($this->_tbl);
1424 
1425  // If the movement delta is negative move the row up.
1426  if ($delta < 0)
1427  {
1428  $query->where('ordering < ' . (int) $this->ordering)
1429  ->order('ordering DESC');
1430  }
1431  // If the movement delta is positive move the row down.
1432  elseif ($delta > 0)
1433  {
1434  $query->where('ordering > ' . (int) $this->ordering)
1435  ->order('ordering ASC');
1436  }
1437 
1438  // Add the custom WHERE clause if set.
1439  if ($where)
1440  {
1441  $query->where($where);
1442  }
1443 
1444  // Select the first row with the criteria.
1445  $this->_db->setQuery($query, 0, 1);
1446  $row = $this->_db->loadObject();
1447 
1448  // If a row is found, move the item.
1449  if (!empty($row))
1450  {
1451  // Update the ordering field for this instance to the row's ordering value.
1452  $query->clear()
1453  ->update($this->_tbl)
1454  ->set('ordering = ' . (int) $row->ordering);
1455  $this->appendPrimaryKeys($query);
1456  $this->_db->setQuery($query);
1457  $this->_db->execute();
1458 
1459  // Update the ordering field for the row to this instance's ordering value.
1460  $query->clear()
1461  ->update($this->_tbl)
1462  ->set('ordering = ' . (int) $this->ordering);
1463  $this->appendPrimaryKeys($query, $row);
1464  $this->_db->setQuery($query);
1465  $this->_db->execute();
1466 
1467  // Update the instance value.
1468  $this->ordering = $row->ordering;
1469  }
1470  else
1471  {
1472  // Update the ordering field for this instance.
1473  $query->clear()
1474  ->update($this->_tbl)
1475  ->set('ordering = ' . (int) $this->ordering);
1476  $this->appendPrimaryKeys($query);
1477  $this->_db->setQuery($query);
1478  $this->_db->execute();
1479  }
1480 
1481  return true;
1482  }
1483 
1484  /**
1485  * Method to set the publishing state for a row or list of rows in the database
1486  * table. The method respects checked out rows by other users and will attempt
1487  * to checkin rows that it can after adjustments are made.
1488  *
1489  * @param mixed $pks An optional array of primary key values to update.
1490  * If not set the instance property value is used.
1491  * @param integer $state The publishing state. eg. [0 = unpublished, 1 = published]
1492  * @param integer $userId The user id of the user performing the operation.
1493  *
1494  * @return boolean True on success; false if $pks is empty.
1495  *
1496  * @link http://docs.joomla.org/JTable/publish
1497  * @since 11.1
1498  */
1499  public function publish($pks = null, $state = 1, $userId = 0)
1500  {
1501  $k = $this->_tbl_keys;
1502 
1503  if (!is_null($pks))
1504  {
1505  foreach ($pks AS $key => $pk)
1506  {
1507  if (!is_array($pk))
1508  {
1509  $pks[$key] = array($this->_tbl_key => $pk);
1510  }
1511  }
1512  }
1513 
1514  $userId = (int) $userId;
1515  $state = (int) $state;
1516 
1517  // If there are no primary keys set check to see if the instance key is set.
1518  if (empty($pks))
1519  {
1520  $pk = array();
1521 
1522  foreach ($this->_tbl_keys AS $key)
1523  {
1524  if ($this->$key)
1525  {
1526  $pk[$this->$key] = $this->$key;
1527  }
1528  // We don't have a full primary key - return false
1529  else
1530  {
1531  return false;
1532  }
1533  }
1534 
1535  $pks = array($pk);
1536  }
1537 
1538  foreach ($pks AS $pk)
1539  {
1540  // Update the publishing state for rows with the given primary keys.
1541  $query = $this->_db->getQuery(true)
1542  ->update($this->_tbl)
1543  ->set('published = ' . (int) $state);
1544 
1545  // Determine if there is checkin support for the table.
1546  if (property_exists($this, 'checked_out') || property_exists($this, 'checked_out_time'))
1547  {
1548  $query->where('(checked_out = 0 OR checked_out = ' . (int) $userId . ')');
1549  $checkin = true;
1550  }
1551  else
1552  {
1553  $checkin = false;
1554  }
1555 
1556  // Build the WHERE clause for the primary keys.
1557  $this->appendPrimaryKeys($query, $pk);
1558 
1559  $this->_db->setQuery($query);
1560  $this->_db->execute();
1561 
1562  // If checkin is supported and all rows were adjusted, check them in.
1563  if ($checkin && (count($pks) == $this->_db->getAffectedRows()))
1564  {
1565  $this->checkin($pk);
1566  }
1567 
1568  $ours = true;
1569 
1570  foreach ($this->_tbl_keys AS $key)
1571  {
1572  if ($this->$key != $pk[$key])
1573  {
1574  $ours = false;
1575  }
1576  }
1577 
1578  if ($ours)
1579  {
1580  $this->published = $state;
1581  }
1582  }
1583 
1584  $this->setError('');
1585 
1586  return true;
1587  }
1588 
1589  /**
1590  * Method to lock the database table for writing.
1591  *
1592  * @return boolean True on success.
1593  *
1594  * @since 11.1
1595  * @throws RuntimeException
1596  */
1597  protected function _lock()
1598  {
1599  $this->_db->lockTable($this->_tbl);
1600  $this->_locked = true;
1601 
1602  return true;
1603  }
1604 
1605  /**
1606  * Method to unlock the database table for writing.
1607  *
1608  * @return boolean True on success.
1609  *
1610  * @since 11.1
1611  */
1612  protected function _unlock()
1613  {
1614  $this->_db->unlockTables();
1615  $this->_locked = false;
1616 
1617  return true;
1618  }
1619 }