| @@ -1,0 +1,958 @@ | ||
| 1 | +<?php | |
| 2 | +/** | |
| 3 | + * @package VikWP - Libraries | |
| 4 | + * @subpackage adapter.database | |
| 5 | + * @author E4J s.r.l. | |
| 6 | + * @copyright Copyright (C) 2023 E4J s.r.l. All Rights Reserved. | |
| 7 | + * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL | |
| 8 | + * @link https://vikwp.com | |
| 9 | + */ | |
| 10 | + | |
| 11 | +// No direct access | |
| 12 | +defined('ABSPATH') or die('No script kiddies please!'); | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * Generic table class for database objects. | |
| 16 | + * | |
| 17 | + * @since 10.1.19 | |
| 18 | + */ | |
| 19 | +class JTable extends JObject | |
| 20 | +{ | |
| 21 | + /** | |
| 22 | + * Include paths for searching for Table classes. | |
| 23 | + * | |
| 24 | + * @var array | |
| 25 | + */ | |
| 26 | + private static $_includePaths = array(); | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * Table fields cache to prevent the static changed applied by PHP 8.1. | |
| 30 | + * @link https://bugs.php.net/bug.php?id=81686 | |
| 31 | + * | |
| 32 | + * @var array | |
| 33 | + * @since 10.1.41 | |
| 34 | + */ | |
| 35 | + protected static $_tableFields = []; | |
| 36 | + | |
| 37 | + /** | |
| 38 | + * Name of the database table to model. | |
| 39 | + * | |
| 40 | + * @var string | |
| 41 | + */ | |
| 42 | + protected $_table = ''; | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * Name of the primary key fields in the table. | |
| 46 | + * | |
| 47 | + * @var array | |
| 48 | + */ | |
| 49 | + protected $_tableKeys = array(); | |
| 50 | + | |
| 51 | + /** | |
| 52 | + * Indicates that the primary keys autoincrement. | |
| 53 | + * | |
| 54 | + * @var boolean | |
| 55 | + */ | |
| 56 | + protected $_autoincrement = true; | |
| 57 | + | |
| 58 | + /** | |
| 59 | + * Array with alias for "special" columns such as ordering, hits etc etc | |
| 60 | + * | |
| 61 | + * @var array | |
| 62 | + * @since 10.1.30 | |
| 63 | + */ | |
| 64 | + protected $_columnAlias = array(); | |
| 65 | + | |
| 66 | + /** | |
| 67 | + * Static method to get an instance of a Table class if it | |
| 68 | + * can be found in the table include paths. | |
| 69 | + * | |
| 70 | + * @param string $type The type (name) of the Table class to get an instance of. | |
| 71 | + * @param string $prefix An optional prefix for the table class name. | |
| 72 | + * @param array $config An optional array of configuration values for the Table object. | |
| 73 | + * | |
| 74 | + * @return mixed A Table object if found, false otherwise. | |
| 75 | + * | |
| 76 | + * @see JTable::addIncludePath() to add include paths for searching for Table classes. | |
| 77 | + */ | |
| 78 | + public static function getInstance($type, $prefix = 'JTable', $config = array()) | |
| 79 | + { | |
| 80 | + // sanitize and prepare the table class name | |
| 81 | + $type = preg_replace('/[^A-Z0-9_\.-]/i', '', $type); | |
| 82 | + $tableClass = $prefix . ucfirst($type); | |
| 83 | + | |
| 84 | + // only try to load the class if it doesn't already exist | |
| 85 | + if (!class_exists($tableClass)) | |
| 86 | + { | |
| 87 | + // search for the class file in the JTable include paths. | |
| 88 | + $paths = static::addIncludePath(); | |
| 89 | + $pathIndex = 0; | |
| 90 | + | |
| 91 | + // iterate until the class is loaded | |
| 92 | + while (!class_exists($tableClass) && $pathIndex < count($paths)) | |
| 93 | + { | |
| 94 | + if ($tryThis = JPath::find($paths[$pathIndex++], strtolower($type) . '.php')) | |
| 95 | + { | |
| 96 | + // import the class file | |
| 97 | + include_once $tryThis; | |
| 98 | + } | |
| 99 | + } | |
| 100 | + | |
| 101 | + if (!class_exists($tableClass)) | |
| 102 | + { | |
| 103 | + throw new Exception(sprintf('Table [%s] not found', $tableClass), 404); | |
| 104 | + } | |
| 105 | + } | |
| 106 | + | |
| 107 | + // instantiate a new table class and return it | |
| 108 | + return new $tableClass($type); | |
| 109 | + } | |
| 110 | + | |
| 111 | + /** | |
| 112 | + * Adds a filesystem path where Table should search for table class files. | |
| 113 | + * | |
| 114 | + * @param mixed $path A filesystem path or array of filesystem paths to add. | |
| 115 | + * | |
| 116 | + * @return array An array of filesystem paths to find Table classes in. | |
| 117 | + */ | |
| 118 | + public static function addIncludePath($path = null) | |
| 119 | + { | |
| 120 | + // if the internal paths have not been initialised, do so with the base table path | |
| 121 | + if (empty(self::$_includePaths)) | |
| 122 | + { | |
| 123 | + self::$_includePaths[] = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'tables'; | |
| 124 | + } | |
| 125 | + | |
| 126 | + if ($path) | |
| 127 | + { | |
| 128 | + // convert the passed path(s) to add to an array. | |
| 129 | + $path = (array) $path; | |
| 130 | + | |
| 131 | + // add each individual new path | |
| 132 | + foreach ($path as $dir) | |
| 133 | + { | |
| 134 | + // sanitize path | |
| 135 | + $dir = trim($dir); | |
| 136 | + | |
| 137 | + // add to the front of the list so that custom paths are searched first | |
| 138 | + if (!in_array($dir, self::$_includePaths)) | |
| 139 | + { | |
| 140 | + array_unshift(self::$_includePaths, $dir); | |
| 141 | + } | |
| 142 | + } | |
| 143 | + } | |
| 144 | + | |
| 145 | + return self::$_includePaths; | |
| 146 | + } | |
| 147 | + | |
| 148 | + /** | |
| 149 | + * Object constructor to set table and key fields. | |
| 150 | + * In most cases this will be overridden by child classes to explicitly | |
| 151 | + * set the table and key fields for a particular database table. | |
| 152 | + * | |
| 153 | + * @param string $table Name of the table to model. | |
| 154 | + * @param mixed $key Name of the primary key field in the table | |
| 155 | + * or array of field names that compose the primary key. | |
| 156 | + */ | |
| 157 | + public function __construct($table, $key = 'id') | |
| 158 | + { | |
| 159 | + // set internal variables | |
| 160 | + $this->_table = $table; | |
| 161 | + | |
| 162 | + // set the key to be an array. | |
| 163 | + if (is_string($key)) | |
| 164 | + { | |
| 165 | + $key = array($key); | |
| 166 | + } | |
| 167 | + else if (is_object($key)) | |
| 168 | + { | |
| 169 | + $key = (array) $key; | |
| 170 | + } | |
| 171 | + | |
| 172 | + $this->_tableKeys = $key; | |
| 173 | + | |
| 174 | + if (count($key) == 1) | |
| 175 | + { | |
| 176 | + $this->_autoincrement = true; | |
| 177 | + } | |
| 178 | + else | |
| 179 | + { | |
| 180 | + $this->_autoincrement = false; | |
| 181 | + } | |
| 182 | + | |
| 183 | + // initialise the table properties | |
| 184 | + $fields = $this->getFields(); | |
| 185 | + | |
| 186 | + if ($fields) | |
| 187 | + { | |
| 188 | + foreach ($fields as $name => $v) | |
| 189 | + { | |
| 190 | + if (is_int($name)) | |
| 191 | + { | |
| 192 | + // use the value in case we have a linear array | |
| 193 | + $name = $v; | |
| 194 | + } | |
| 195 | + | |
| 196 | + // add the field if it is not already present | |
| 197 | + if (!property_exists($this, $name)) | |
| 198 | + { | |
| 199 | + $this->{$name} = null; | |
| 200 | + } | |
| 201 | + } | |
| 202 | + } | |
| 203 | + } | |
| 204 | + | |
| 205 | + /** | |
| 206 | + * Gets the columns from database table. | |
| 207 | + * | |
| 208 | + * @param boolean $reload Flag to reload cache. | |
| 209 | + * | |
| 210 | + * @return mixed An array of the field names, or false if an error occurs. | |
| 211 | + * | |
| 212 | + * @throws Exception | |
| 213 | + */ | |
| 214 | + public function getFields($reload = false) | |
| 215 | + { | |
| 216 | + $key = $this->getTableName(); | |
| 217 | + | |
| 218 | + if (!isset(static::$_tableFields[$key]) || $reload) | |
| 219 | + { | |
| 220 | + $dbo = JFactory::getDbo(); | |
| 221 | + | |
| 222 | + // lookup the fields for this table only once | |
| 223 | + $fields = $dbo->getTableColumns($this->_table, false); | |
| 224 | + | |
| 225 | + if (empty($fields)) | |
| 226 | + { | |
| 227 | + throw new Exception(sprintf('No columns found for [%s] table', $this->_table)); | |
| 228 | + } | |
| 229 | + | |
| 230 | + static::$_tableFields[$key] = $fields; | |
| 231 | + } | |
| 232 | + | |
| 233 | + return static::$_tableFields[$key]; | |
| 234 | + } | |
| 235 | + | |
| 236 | + /** | |
| 237 | + * Method to get the database table name for the class. | |
| 238 | + * | |
| 239 | + * @return string The name of the database table being modeled. | |
| 240 | + * | |
| 241 | + * @since 10.1.30 | |
| 242 | + */ | |
| 243 | + public function getTableName() | |
| 244 | + { | |
| 245 | + return $this->_table; | |
| 246 | + } | |
| 247 | + | |
| 248 | + /** | |
| 249 | + * Method to get the primary key field name for the table. | |
| 250 | + * | |
| 251 | + * @param boolean $multiple True to return all primary keys (as an array) or false to return | |
| 252 | + * just the first one (as a string). | |
| 253 | + * | |
| 254 | + * @return mixed Array of primary key field names or string containing the first primary key field. | |
| 255 | + * | |
| 256 | + * @since 10.1.35 | |
| 257 | + */ | |
| 258 | + public function getKeyName($multiple = false) | |
| 259 | + { | |
| 260 | + // Count the number of keys | |
| 261 | + if (count($this->_tableKeys)) | |
| 262 | + { | |
| 263 | + if ($multiple) | |
| 264 | + { | |
| 265 | + // If we want multiple keys, return the raw array. | |
| 266 | + return $this->_tableKeys; | |
| 267 | + } | |
| 268 | + else | |
| 269 | + { | |
| 270 | + // If we want the standard method, just return the first key. | |
| 271 | + return $this->_tableKeys[0]; | |
| 272 | + } | |
| 273 | + } | |
| 274 | + | |
| 275 | + return ''; | |
| 276 | + } | |
| 277 | + | |
| 278 | + /** | |
| 279 | + * Validate that the primary key has been set. | |
| 280 | + * | |
| 281 | + * @return boolean True if the primary key(s) have been set. | |
| 282 | + * | |
| 283 | + * @since 10.1.30 | |
| 284 | + */ | |
| 285 | + public function hasPrimaryKey() | |
| 286 | + { | |
| 287 | + if ($this->_autoincrement) | |
| 288 | + { | |
| 289 | + $empty = true; | |
| 290 | + | |
| 291 | + foreach ($this->_tableKeys as $key) | |
| 292 | + { | |
| 293 | + $empty = $empty && empty($this->$key); | |
| 294 | + } | |
| 295 | + } | |
| 296 | + else | |
| 297 | + { | |
| 298 | + $dbo = JFactory::getDbo(); | |
| 299 | + | |
| 300 | + $q = $dbo->getQuery(true) | |
| 301 | + ->select('COUNT(*)') | |
| 302 | + ->from($this->_table); | |
| 303 | + | |
| 304 | + $this->appendPrimaryKeys($q); | |
| 305 | + | |
| 306 | + $dbo->setQuery($q); | |
| 307 | + | |
| 308 | + $count = $dbo->loadResult(); | |
| 309 | + | |
| 310 | + if ($count == 1) | |
| 311 | + { | |
| 312 | + $empty = false; | |
| 313 | + } | |
| 314 | + else | |
| 315 | + { | |
| 316 | + $empty = true; | |
| 317 | + } | |
| 318 | + } | |
| 319 | + | |
| 320 | + return !$empty; | |
| 321 | + } | |
| 322 | + | |
| 323 | + /** | |
| 324 | + * Method to append the primary keys for this table to a query. | |
| 325 | + * | |
| 326 | + * @param mixed $query A query object to append. | |
| 327 | + * @param mixed $pk Optional primary key parameter. | |
| 328 | + * | |
| 329 | + * @return void | |
| 330 | + * | |
| 331 | + * @since 10.1.30 | |
| 332 | + */ | |
| 333 | + public function appendPrimaryKeys($query, $pk = null) | |
| 334 | + { | |
| 335 | + $dbo = JFactory::getDbo(); | |
| 336 | + | |
| 337 | + if (is_null($pk)) | |
| 338 | + { | |
| 339 | + foreach ($this->_tableKeys as $k) | |
| 340 | + { | |
| 341 | + $query->where($dbo->qn($k) . ' = ' . $dbo->q($this->{$k})); | |
| 342 | + } | |
| 343 | + } | |
| 344 | + else | |
| 345 | + { | |
| 346 | + if (is_string($pk)) | |
| 347 | + { | |
| 348 | + $pk = array($this->_tableKeys[0] => $pk); | |
| 349 | + } | |
| 350 | + | |
| 351 | + $pk = (object) $pk; | |
| 352 | + | |
| 353 | + foreach ($this->_tableKeys as $k) | |
| 354 | + { | |
| 355 | + $query->where($dbo->qn($k) . ' = ' . $dbo->q($pk->{$k})); | |
| 356 | + } | |
| 357 | + } | |
| 358 | + } | |
| 359 | + | |
| 360 | + /** | |
| 361 | + * Method to reset class properties to the defaults set in the class | |
| 362 | + * definition. It will ignore the primary key as well as any private class | |
| 363 | + * properties (except $_errors). | |
| 364 | + * | |
| 365 | + * @return void | |
| 366 | + * | |
| 367 | + * @since 10.1.30 | |
| 368 | + */ | |
| 369 | + public function reset() | |
| 370 | + { | |
| 371 | + // get the default values for the class from the table | |
| 372 | + foreach ($this->getFields() as $k => $v) | |
| 373 | + { | |
| 374 | + // if the property is not the primary key or private, reset it | |
| 375 | + if (!in_array($k, $this->_tableKeys) && (strpos($k, '_') !== 0)) | |
| 376 | + { | |
| 377 | + $this->{$k} = $v->Default; | |
| 378 | + } | |
| 379 | + } | |
| 380 | + | |
| 381 | + // reset table errors | |
| 382 | + $this->_errors = array(); | |
| 383 | + } | |
| 384 | + | |
| 385 | + /** | |
| 386 | + * Method to bind an associative array or object to the Table instance.This | |
| 387 | + * method only binds properties that are publicly accessible and optionally | |
| 388 | + * takes an array of properties to ignore when binding. | |
| 389 | + * | |
| 390 | + * @param mixed $src An associative array or object to bind to the Table instance. | |
| 391 | + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. | |
| 392 | + * | |
| 393 | + * @return boolean True on success. | |
| 394 | + * | |
| 395 | + * @since 10.1.30 | |
| 396 | + * | |
| 397 | + * @throws InvalidArgumentException | |
| 398 | + */ | |
| 399 | + public function bind($src, $ignore = array()) | |
| 400 | + { | |
| 401 | + // check if the source value is an array or object | |
| 402 | + if (!is_object($src) && !is_array($src)) | |
| 403 | + { | |
| 404 | + throw new InvalidArgumentException( | |
| 405 | + sprintf( | |
| 406 | + 'Could not bind the data source in %s::bind(), the source must be an array or object but a "%s" was given.', | |
| 407 | + get_class($this), | |
| 408 | + gettype($src) | |
| 409 | + ) | |
| 410 | + ); | |
| 411 | + } | |
| 412 | + | |
| 413 | + // if the source value is an object, get its accessible properties | |
| 414 | + if (is_object($src)) | |
| 415 | + { | |
| 416 | + $src = get_object_vars($src); | |
| 417 | + } | |
| 418 | + | |
| 419 | + $ignore = (array) $ignore; | |
| 420 | + | |
| 421 | + // bind the source value, excluding the ignored fields | |
| 422 | + foreach ($this->getProperties() as $k => $v) | |
| 423 | + { | |
| 424 | + // only process fields not in the ignore array | |
| 425 | + if (!in_array($k, $ignore)) | |
| 426 | + { | |
| 427 | + if (isset($src[$k])) | |
| 428 | + { | |
| 429 | + $this->{$k} = $src[$k]; | |
| 430 | + } | |
| 431 | + } | |
| 432 | + } | |
| 433 | + | |
| 434 | + return true; | |
| 435 | + } | |
| 436 | + | |
| 437 | + /** | |
| 438 | + * Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database. | |
| 439 | + * | |
| 440 | + * Child classes should override this method to make sure the data they are storing in the database is safe and as expected before storage. | |
| 441 | + * | |
| 442 | + * @return boolean True if the instance is sane and able to be stored in the database. | |
| 443 | + * | |
| 444 | + * @since 10.1.30 | |
| 445 | + */ | |
| 446 | + public function check() | |
| 447 | + { | |
| 448 | + // inherit in children classes | |
| 449 | + return true; | |
| 450 | + } | |
| 451 | + | |
| 452 | + /** | |
| 453 | + * Method to store a row in the database from the Table instance properties. | |
| 454 | + * | |
| 455 | + * If a primary key value is set the row with that primary key value will be updated with the instance property values. | |
| 456 | + * If no primary key value is set a new row will be inserted into the database with the properties from the Table instance. | |
| 457 | + * | |
| 458 | + * @param boolean $updateNulls True to update fields even if they are null. | |
| 459 | + * | |
| 460 | + * @return boolean True on success. | |
| 461 | + * | |
| 462 | + * @since 10.1.30 | |
| 463 | + */ | |
| 464 | + public function store($updateNulls = false) | |
| 465 | + { | |
| 466 | + $dbo = JFactory::getDbo(); | |
| 467 | + $event = JEventDispatcher::getInstance(); | |
| 468 | + | |
| 469 | + $k = $this->_tableKeys; | |
| 470 | + | |
| 471 | + // pre-processing by observers | |
| 472 | + $event->trigger('onBeforeStore', array($updateNulls, $k)); | |
| 473 | + | |
| 474 | + // If a primary key exists update the object, otherwise insert it. | |
| 475 | + if ($this->hasPrimaryKey()) | |
| 476 | + { | |
| 477 | + $result = $dbo->updateObject($this->_table, $this, $this->_tableKeys, $updateNulls); | |
| 478 | + } | |
| 479 | + else | |
| 480 | + { | |
| 481 | + $result = $dbo->insertObject($this->_table, $this, $this->_tableKeys[0]); | |
| 482 | + } | |
| 483 | + | |
| 484 | + /** | |
| 485 | + * The table is now able to propagate the error message faced during | |
| 486 | + * the database insert/update execution. | |
| 487 | + * | |
| 488 | + * @since 10.1.58 | |
| 489 | + */ | |
| 490 | + if (!$result) | |
| 491 | + { | |
| 492 | + // propagate error message | |
| 493 | + $this->setError($dbo->getLastError() ?: 'Database query error.'); | |
| 494 | + } | |
| 495 | + | |
| 496 | + // post-processing by observers | |
| 497 | + $event->trigger('onAfterStore', array(&$result)); | |
| 498 | + | |
| 499 | + return $result; | |
| 500 | + } | |
| 501 | + | |
| 502 | + /** | |
| 503 | + * Method to provide a shortcut to binding, checking and storing a Table instance to the database table. | |
| 504 | + * | |
| 505 | + * The method will check a row in once the data has been stored and if an ordering filter is present will attempt to reorder | |
| 506 | + * the table rows based on the filter. The ordering filter is an instance property name. The rows that will be reordered | |
| 507 | + * are those whose value matches the Table instance for the property specified. | |
| 508 | + * | |
| 509 | + * @param mixed $src An associative array or object to bind to the Table instance. | |
| 510 | + * @param string $orderingFilter Filter for the order updating | |
| 511 | + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. | |
| 512 | + * | |
| 513 | + * @return boolean True on success. | |
| 514 | + * | |
| 515 | + * @since 10.1.30 | |
| 516 | + */ | |
| 517 | + public function save($src, $orderingFilter = '', $ignore = '') | |
| 518 | + { | |
| 519 | + // attempt to bind the source to the instance | |
| 520 | + if (!$this->bind($src, $ignore)) | |
| 521 | + { | |
| 522 | + return false; | |
| 523 | + } | |
| 524 | + | |
| 525 | + // run any sanity checks on the instance and verify that it is ready for storage | |
| 526 | + if (!$this->check()) | |
| 527 | + { | |
| 528 | + return false; | |
| 529 | + } | |
| 530 | + | |
| 531 | + // attempt to store the properties to the database table | |
| 532 | + if (!$this->store()) | |
| 533 | + { | |
| 534 | + return false; | |
| 535 | + } | |
| 536 | + | |
| 537 | + // ff an ordering filter is set, attempt reorder the rows in the table based on the filter and value. | |
| 538 | + if ($orderingFilter) | |
| 539 | + { | |
| 540 | + $dbo = JFactory::getDbo(); | |
| 541 | + | |
| 542 | + $filterValue = $this->{$orderingFilter}; | |
| 543 | + $this->reorder($orderingFilter ? $dbo->qn($orderingFilter) . ' = ' . $dbo->q($filterValue) : ''); | |
| 544 | + } | |
| 545 | + | |
| 546 | + return true; | |
| 547 | + } | |
| 548 | + | |
| 549 | + /** | |
| 550 | + * Method to delete a row from the database table by primary key value. | |
| 551 | + * | |
| 552 | + * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. | |
| 553 | + * | |
| 554 | + * @return boolean True on success. | |
| 555 | + * | |
| 556 | + * @since 10.1.30 | |
| 557 | + * | |
| 558 | + * @throws UnexpectedValueException | |
| 559 | + */ | |
| 560 | + public function delete($pk = null) | |
| 561 | + { | |
| 562 | + if (is_null($pk)) | |
| 563 | + { | |
| 564 | + $pk = array(); | |
| 565 | + | |
| 566 | + foreach ($this->_tableKeys as $key) | |
| 567 | + { | |
| 568 | + $pk[$key] = $this->{$key}; | |
| 569 | + } | |
| 570 | + } | |
| 571 | + else if (!is_array($pk)) | |
| 572 | + { | |
| 573 | + $pk = array($this->_tableKeys[0] => $pk); | |
| 574 | + } | |
| 575 | + | |
| 576 | + foreach ($this->_tableKeys as $key) | |
| 577 | + { | |
| 578 | + $pk[$key] = is_null($pk[$key]) ? $this->{$key} : $pk[$key]; | |
| 579 | + | |
| 580 | + if ($pk[$key] === null) | |
| 581 | + { | |
| 582 | + throw new UnexpectedValueException('Null primary key not allowed.'); | |
| 583 | + } | |
| 584 | + | |
| 585 | + $this->{$key} = $pk[$key]; | |
| 586 | + } | |
| 587 | + | |
| 588 | + $dbo = JFactory::getDbo(); | |
| 589 | + $event = JEventDispatcher::getInstance(); | |
| 590 | + | |
| 591 | + // pre-processing by observers | |
| 592 | + $event->trigger('onBeforeDelete', array($pk)); | |
| 593 | + | |
| 594 | + // delete the row by primary key | |
| 595 | + $q = $dbo->getQuery(true) | |
| 596 | + ->delete($this->_table); | |
| 597 | + | |
| 598 | + $this->appendPrimaryKeys($q, $pk); | |
| 599 | + | |
| 600 | + $dbo->setQuery($q); | |
| 601 | + | |
| 602 | + // check for a database error | |
| 603 | + $dbo->execute(); | |
| 604 | + | |
| 605 | + // post-processing by observers | |
| 606 | + $event->trigger('onAfterDelete', array($pk)); | |
| 607 | + | |
| 608 | + return true; | |
| 609 | + } | |
| 610 | + | |
| 611 | + /** | |
| 612 | + * Method to get the next ordering value for a group of rows defined by an SQL WHERE clause. | |
| 613 | + * | |
| 614 | + * This is useful for placing a new item last in a group of items in the table. | |
| 615 | + * | |
| 616 | + * @param string $where WHERE clause to use for selecting the MAX(ordering) for the table. | |
| 617 | + * | |
| 618 | + * @return integer The next ordering value. | |
| 619 | + * | |
| 620 | + * @since 10.1.30 | |
| 621 | + * | |
| 622 | + * @throws UnexpectedValueException | |
| 623 | + */ | |
| 624 | + public function getNextOrder($where = '') | |
| 625 | + { | |
| 626 | + // check if there is an ordering field set | |
| 627 | + $orderingField = $this->getColumnAlias('ordering'); | |
| 628 | + | |
| 629 | + if (!property_exists($this, $orderingField)) | |
| 630 | + { | |
| 631 | + throw new UnexpectedValueException(sprintf('%s does not support ordering.', get_class($this))); | |
| 632 | + } | |
| 633 | + | |
| 634 | + $dbo = JFactory::getDbo(); | |
| 635 | + | |
| 636 | + // get the largest ordering value for a given where clause | |
| 637 | + $q = $dbo->getQuery(true) | |
| 638 | + ->select('MAX(' . $dbo->qn($orderingField) . ')') | |
| 639 | + ->from($this->_table); | |
| 640 | + | |
| 641 | + if ($where) | |
| 642 | + { | |
| 643 | + $q->where($where); | |
| 644 | + } | |
| 645 | + | |
| 646 | + $dbo->setQuery($q); | |
| 647 | + $max = (int) $dbo->loadResult(); | |
| 648 | + | |
| 649 | + // return the largest ordering value + 1 | |
| 650 | + return $max + 1; | |
| 651 | + } | |
| 652 | + | |
| 653 | + /** | |
| 654 | + * Method to compact the ordering values of rows in a group of rows defined by an SQL WHERE clause. | |
| 655 | + * | |
| 656 | + * @param string $where WHERE clause to use for limiting the selection of rows to compact the ordering values. | |
| 657 | + * | |
| 658 | + * @return mixed Boolean True on success. | |
| 659 | + * | |
| 660 | + * @since 10.1.30 | |
| 661 | + * | |
| 662 | + * @throws UnexpectedValueException | |
| 663 | + */ | |
| 664 | + public function reorder($where = '') | |
| 665 | + { | |
| 666 | + // check if there is an ordering field set | |
| 667 | + $orderingField = $this->getColumnAlias('ordering'); | |
| 668 | + | |
| 669 | + if (!property_exists($this, $orderingField)) | |
| 670 | + { | |
| 671 | + throw new UnexpectedValueException(sprintf('%s does not support ordering.', get_class($this))); | |
| 672 | + } | |
| 673 | + | |
| 674 | + $dbo = JFactory::getDbo(); | |
| 675 | + | |
| 676 | + $quotedOrderingField = $dbo->qn($orderingField); | |
| 677 | + | |
| 678 | + $subquery = $dbo->getQuery(true) | |
| 679 | + ->from($this->_table) | |
| 680 | + ->selectRowNumber($quotedOrderingField, 'new_ordering'); | |
| 681 | + | |
| 682 | + $query = $dbo->getQuery(true) | |
| 683 | + ->update($this->_table) | |
| 684 | + ->set($quotedOrderingField . ' = sq.new_ordering'); | |
| 685 | + | |
| 686 | + $innerOn = array(); | |
| 687 | + | |
| 688 | + // get the primary keys for the selection | |
| 689 | + foreach ($this->_tableKeys as $i => $k) | |
| 690 | + { | |
| 691 | + $subquery->select($dbo->qn($k, 'pk__' . $i)); | |
| 692 | + $innerOn[] = $dbo->qn($k) . ' = sq.' . $dbo->qn('pk__' . $i); | |
| 693 | + } | |
| 694 | + | |
| 695 | + // setup the extra where and ordering clause data | |
| 696 | + if ($where) | |
| 697 | + { | |
| 698 | + $subquery->where($where); | |
| 699 | + $query->where($where); | |
| 700 | + } | |
| 701 | + | |
| 702 | + $subquery->where($quotedOrderingField . ' >= 0'); | |
| 703 | + $query->where($quotedOrderingField . ' >= 0'); | |
| 704 | + | |
| 705 | + $query->innerJoin('(' . (string) $subquery . ') AS sq ON ' . implode(' AND ', $innerOn)); | |
| 706 | + | |
| 707 | + $dbo->setQuery($query); | |
| 708 | + $dbo->execute(); | |
| 709 | + } | |
| 710 | + | |
| 711 | + /** | |
| 712 | + * Method to set the publishing state for a row or list of rows in the database table. | |
| 713 | + * | |
| 714 | + * The method respects checked out rows by other users and will attempt to checkin rows that it can after adjustments are made. | |
| 715 | + * | |
| 716 | + * @param mixed $pks An optional array of primary key values to update. If not set the instance property value is used. | |
| 717 | + * @param integer $state The publishing state. eg. [0 = unpublished, 1 = published] | |
| 718 | + * @param integer $userId The user ID of the user performing the operation. | |
| 719 | + * | |
| 720 | + * @return boolean True on success; false if $pks is empty. | |
| 721 | + * | |
| 722 | + * @since 10.1.30 | |
| 723 | + */ | |
| 724 | + public function publish($pks = null, $state = 1, $userId = 0) | |
| 725 | + { | |
| 726 | + // sanitize input | |
| 727 | + $userId = (int) $userId; | |
| 728 | + $state = (int) $state; | |
| 729 | + | |
| 730 | + if (!is_null($pks)) | |
| 731 | + { | |
| 732 | + if (!is_array($pks)) | |
| 733 | + { | |
| 734 | + $pks = array($pks); | |
| 735 | + } | |
| 736 | + | |
| 737 | + foreach ($pks as $key => $pk) | |
| 738 | + { | |
| 739 | + if (!is_array($pk)) | |
| 740 | + { | |
| 741 | + $pks[$key] = array($this->_tableKeys[0] => $pk); | |
| 742 | + } | |
| 743 | + } | |
| 744 | + } | |
| 745 | + | |
| 746 | + // if there are no primary keys set check to see if the instance key is set | |
| 747 | + if (empty($pks)) | |
| 748 | + { | |
| 749 | + $pk = array(); | |
| 750 | + | |
| 751 | + foreach ($this->_tableKeys as $key) | |
| 752 | + { | |
| 753 | + if ($this->{$key}) | |
| 754 | + { | |
| 755 | + $pk[$key] = $this->{$key}; | |
| 756 | + } | |
| 757 | + // we don't have a full primary key - return false | |
| 758 | + else | |
| 759 | + { | |
| 760 | + $this->setError('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED'); | |
| 761 | + | |
| 762 | + return false; | |
| 763 | + } | |
| 764 | + } | |
| 765 | + | |
| 766 | + $pks = array($pk); | |
| 767 | + } | |
| 768 | + | |
| 769 | + $dbo = JFactory::getDbo(); | |
| 770 | + | |
| 771 | + $publishedField = $this->getColumnAlias('published'); | |
| 772 | + | |
| 773 | + foreach ($pks as $pk) | |
| 774 | + { | |
| 775 | + // update the publishing state for rows with the given primary keys | |
| 776 | + $query = $dbo->getQuery(true) | |
| 777 | + ->update($this->_table) | |
| 778 | + ->set($dbo->qn($publishedField) . ' = ' . (int) $state); | |
| 779 | + | |
| 780 | + // build the WHERE clause for the primary keys | |
| 781 | + $this->appendPrimaryKeys($query, $pk); | |
| 782 | + | |
| 783 | + $dbo->setQuery($query); | |
| 784 | + | |
| 785 | + try | |
| 786 | + { | |
| 787 | + $dbo->execute(); | |
| 788 | + } | |
| 789 | + catch (RuntimeException $e) | |
| 790 | + { | |
| 791 | + $this->setError($e->getMessage()); | |
| 792 | + | |
| 793 | + return false; | |
| 794 | + } | |
| 795 | + | |
| 796 | + // if the Table instance value is in the list of primary keys that were set, set the instance | |
| 797 | + $ours = true; | |
| 798 | + | |
| 799 | + foreach ($this->_tableKeys as $key) | |
| 800 | + { | |
| 801 | + if ($this->{$key} != $pk[$key]) | |
| 802 | + { | |
| 803 | + $ours = false; | |
| 804 | + } | |
| 805 | + } | |
| 806 | + | |
| 807 | + if ($ours) | |
| 808 | + { | |
| 809 | + $this->{$publishedField} = $state; | |
| 810 | + } | |
| 811 | + } | |
| 812 | + | |
| 813 | + return true; | |
| 814 | + } | |
| 815 | + | |
| 816 | + /** | |
| 817 | + * Method to return the real name of a "special" column such as ordering, hits, published | |
| 818 | + * etc etc. In this way you are free to follow your db naming convention and use the | |
| 819 | + * built in \Joomla functions. | |
| 820 | + * | |
| 821 | + * @param string $column Name of the "special" column (ie ordering, hits). | |
| 822 | + * | |
| 823 | + * @return string The string that identify the special. | |
| 824 | + * | |
| 825 | + * @since 10.1.30 | |
| 826 | + */ | |
| 827 | + public function getColumnAlias($column) | |
| 828 | + { | |
| 829 | + // get the column data if set | |
| 830 | + if (isset($this->_columnAlias[$column])) | |
| 831 | + { | |
| 832 | + $return = $this->_columnAlias[$column]; | |
| 833 | + } | |
| 834 | + else | |
| 835 | + { | |
| 836 | + $return = $column; | |
| 837 | + } | |
| 838 | + | |
| 839 | + // sanitize the name | |
| 840 | + $return = preg_replace('#[^A-Z0-9_]#i', '', $return); | |
| 841 | + | |
| 842 | + return $return; | |
| 843 | + } | |
| 844 | + | |
| 845 | + /** | |
| 846 | + * Method to register a column alias for a "special" column. | |
| 847 | + * | |
| 848 | + * @param string $column The "special" column (i.e. ordering). | |
| 849 | + * @param string $columnAlias The real column name (i.e. foo_ordering). | |
| 850 | + * | |
| 851 | + * @return void | |
| 852 | + * | |
| 853 | + * @since 10.1.30 | |
| 854 | + */ | |
| 855 | + public function setColumnAlias($column, $columnAlias) | |
| 856 | + { | |
| 857 | + // santize the column name alias | |
| 858 | + $column = strtolower($column); | |
| 859 | + $column = preg_replace('#[^A-Z0-9_]#i', '', $column); | |
| 860 | + | |
| 861 | + // set the column alias internally | |
| 862 | + $this->_columnAlias[$column] = $columnAlias; | |
| 863 | + } | |
| 864 | + | |
| 865 | + /** | |
| 866 | + * Method to load a row from the database by primary key and bind the fields to the Table instance properties. | |
| 867 | + * | |
| 868 | + * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. | |
| 869 | + * If not set the instance property value is used. | |
| 870 | + * @param boolean $reset True to reset the default values before loading the new row. | |
| 871 | + * | |
| 872 | + * @return boolean True if successful. False if row not found. | |
| 873 | + * | |
| 874 | + * @since 10.1.35 | |
| 875 | + */ | |
| 876 | + public function load($keys = null, $reset = true) | |
| 877 | + { | |
| 878 | + if (empty($keys)) | |
| 879 | + { | |
| 880 | + $empty = true; | |
| 881 | + $keys = array(); | |
| 882 | + | |
| 883 | + // If empty, use the value of the current key | |
| 884 | + foreach ($this->_tableKeys as $key) | |
| 885 | + { | |
| 886 | + $empty = $empty && empty($this->$key); | |
| 887 | + $keys[$key] = $this->$key; | |
| 888 | + } | |
| 889 | + | |
| 890 | + // If empty primary key there's is no need to load anything | |
| 891 | + if ($empty) | |
| 892 | + { | |
| 893 | + return true; | |
| 894 | + } | |
| 895 | + } | |
| 896 | + else if (!is_array($keys)) | |
| 897 | + { | |
| 898 | + // Load by primary key. | |
| 899 | + $keyCount = count($this->_tableKeys); | |
| 900 | + | |
| 901 | + if ($keyCount) | |
| 902 | + { | |
| 903 | + if ($keyCount > 1) | |
| 904 | + { | |
| 905 | + throw new InvalidArgumentException('Table has multiple primary keys specified, only one primary key value provided.'); | |
| 906 | + } | |
| 907 | + | |
| 908 | + $keys = array($this->getKeyName() => $keys); | |
| 909 | + } | |
| 910 | + else | |
| 911 | + { | |
| 912 | + throw new RuntimeException('No table keys defined.'); | |
| 913 | + } | |
| 914 | + } | |
| 915 | + | |
| 916 | + if ($reset) | |
| 917 | + { | |
| 918 | + $this->reset(); | |
| 919 | + } | |
| 920 | + | |
| 921 | + $dbo = JFactory::getDbo(); | |
| 922 | + | |
| 923 | + // Initialise the query. | |
| 924 | + $query = $dbo->getQuery(true) | |
| 925 | + ->select('*') | |
| 926 | + ->from($this->_table); | |
| 927 | + $fields = array_keys($this->getProperties()); | |
| 928 | + | |
| 929 | + foreach ($keys as $field => $value) | |
| 930 | + { | |
| 931 | + // Check that $field is in the table. | |
| 932 | + if (!in_array($field, $fields)) | |
| 933 | + { | |
| 934 | + throw new UnexpectedValueException(sprintf('Missing field in database: %s   %s.', get_class($this), $field)); | |
| 935 | + } | |
| 936 | + | |
| 937 | + // Add the search tuple to the query. | |
| 938 | + $query->where($dbo->quoteName($field) . ' = ' . $dbo->quote($value)); | |
| 939 | + } | |
| 940 | + | |
| 941 | + $dbo->setQuery($query); | |
| 942 | + | |
| 943 | + $row = $dbo->loadAssoc(); | |
| 944 | + | |
| 945 | + // check that we have a result | |
| 946 | + if (empty($row)) | |
| 947 | + { | |
| 948 | + $result = false; | |
| 949 | + } | |
| 950 | + else | |
| 951 | + { | |
| 952 | + // Bind the object with the row and return. | |
| 953 | + $result = $this->bind($row); | |
| 954 | + } | |
| 955 | + | |
| 956 | + return $result; | |
| 957 | + } | |
| 958 | +} | |