[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/libraries/src/Table/ -> Table.php (source)

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


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer