[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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   %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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |