1<?php 2 3/* 4 * This file is part of the TYPO3 CMS project. 5 * 6 * It is free software; you can redistribute it and/or modify it under 7 * the terms of the GNU General Public License, either version 2 8 * of the License, or any later version. 9 * 10 * For the full copyright and license information, please read the 11 * LICENSE.txt file that was distributed with this source code. 12 * 13 * The TYPO3 project - inspiring people to share! 14 */ 15 16namespace TYPO3\CMS\Core\Collection; 17 18use TYPO3\CMS\Core\Database\ConnectionPool; 19use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; 20use TYPO3\CMS\Core\DataHandling\DataHandler; 21use TYPO3\CMS\Core\Utility\GeneralUtility; 22 23/** 24 * Abstract implementation of a RecordCollection 25 * 26 * RecordCollection is a collections of TCA-Records. 27 * The collection is meant to be stored in TCA-table sys_file_collections and is manageable 28 * via FormEngine. 29 * 30 * A RecordCollection might be used to group a set of records (e.g. news, images, contentElements) 31 * for output in frontend 32 * 33 * The AbstractRecordCollection uses SplDoublyLinkedList for internal storage 34 * 35 * @template T 36 * @implements RecordCollectionInterface<T> 37 */ 38abstract class AbstractRecordCollection implements RecordCollectionInterface, PersistableCollectionInterface, SortableCollectionInterface 39{ 40 /** 41 * The table name collections are stored to 42 * 43 * @var string 44 */ 45 protected static $storageItemsField = 'items'; 46 47 /** 48 * The table name collections are stored to, must be defined in the subclass 49 * 50 * @var string 51 */ 52 protected static $storageTableName = ''; 53 54 /** 55 * Uid of the storage 56 * 57 * @var int 58 */ 59 protected $uid = 0; 60 61 /** 62 * Collection title 63 * 64 * @var string 65 */ 66 protected $title; 67 68 /** 69 * Collection description 70 * 71 * @var string 72 */ 73 protected $description; 74 75 /** 76 * Table name of the records stored in this collection 77 * 78 * @var string 79 */ 80 protected $itemTableName; 81 82 /** 83 * The local storage 84 * 85 * @var \SplDoublyLinkedList 86 */ 87 protected $storage; 88 89 /** 90 * Creates this object. 91 */ 92 public function __construct() 93 { 94 $this->storage = new \SplDoublyLinkedList(); 95 } 96 97 /** 98 * (PHP 5 >= 5.1.0) 99 * Return the current element 100 * 101 * @link https://php.net/manual/en/iterator.current.php 102 * @return mixed Can return any type. 103 * @todo: Set return type to mixed when PHP >= 8.0 is required and drop #[\ReturnTypeWillChange] 104 */ 105 #[\ReturnTypeWillChange] 106 public function current() 107 { 108 return $this->storage->current(); 109 } 110 111 /** 112 * (PHP 5 >= 5.1.0) 113 * Move forward to next element 114 * 115 * @link https://php.net/manual/en/iterator.next.php 116 * @todo: Set return type to void in v12 as breaking patch and drop #[\ReturnTypeWillChange] 117 */ 118 #[\ReturnTypeWillChange] 119 public function next() 120 { 121 $this->storage->next(); 122 } 123 124 /** 125 * (PHP 5 >= 5.1.0) 126 * Return the key of the current element 127 * 128 * @link https://php.net/manual/en/iterator.key.php 129 * @return int|string 0 on failure. 130 * @todo: Set return type to mixed when PHP >= 8.0 is required and drop #[\ReturnTypeWillChange] 131 */ 132 #[\ReturnTypeWillChange] 133 public function key() 134 { 135 $currentRecord = $this->storage->current(); 136 return $currentRecord['uid'] ?? 0; 137 } 138 139 /** 140 * (PHP 5 >= 5.1.0) 141 * Checks if current position is valid 142 * 143 * @link https://php.net/manual/en/iterator.valid.php 144 * @return bool The return value will be casted to boolean and then evaluated. 145 * @todo: Set return type to bool in v12 as breaking patch and drop #[\ReturnTypeWillChange] 146 */ 147 #[\ReturnTypeWillChange] 148 public function valid() 149 { 150 return $this->storage->valid(); 151 } 152 153 /** 154 * (PHP 5 >= 5.1.0) 155 * Rewind the Iterator to the first element 156 * 157 * @link https://php.net/manual/en/iterator.rewind.php 158 * @todo: Set return type to void in v12 as breaking patch and drop #[\ReturnTypeWillChange] 159 */ 160 #[\ReturnTypeWillChange] 161 public function rewind() 162 { 163 $this->storage->rewind(); 164 } 165 166 /** 167 * (PHP 5 >= 5.1.0) 168 * String representation of object 169 * 170 * @link https://php.net/manual/en/serializable.serialize.php 171 * @return string the string representation of the object or &null; 172 * @todo: Drop method and \Serializable (through parent inteface) class interface in v12. 173 */ 174 public function serialize() 175 { 176 return serialize($this->__serialize()); 177 } 178 179 /** 180 * Returns class state to be serialized. 181 */ 182 public function __serialize(): array 183 { 184 return [ 185 'uid' => $this->getIdentifier(), 186 ]; 187 } 188 189 /** 190 * (PHP 5 >= 5.1.0) 191 * Constructs the object 192 * 193 * @link https://php.net/manual/en/serializable.unserialize.php 194 * @param string $serialized The string representation of the object 195 * @return mixed the original value unserialized. 196 * @todo: Drop method and \Serializable (through parent interface) class interface in v12. 197 */ 198 public function unserialize($serialized) 199 { 200 $this->__unserialize(unserialize($serialized)); 201 } 202 203 /** 204 * Load records with the given serialized information 205 */ 206 public function __unserialize(array $arrayRepresentation): void 207 { 208 self::load($arrayRepresentation['uid']); 209 } 210 211 /** 212 * (PHP 5 >= 5.1.0) 213 * Count elements of an object 214 * 215 * @link https://php.net/manual/en/countable.count.php 216 * @return int The custom count as an integer. 217 * @todo: Set return type to in in v12 as breaking patch and drop #[\ReturnTypeWillChange] 218 */ 219 #[\ReturnTypeWillChange] 220 public function count() 221 { 222 return $this->storage->count(); 223 } 224 225 /** 226 * Getter for the title 227 * 228 * @return string 229 */ 230 public function getTitle() 231 { 232 return $this->title; 233 } 234 235 /** 236 * Getter for the UID 237 * 238 * @return int 239 */ 240 public function getUid() 241 { 242 return $this->uid; 243 } 244 245 /** 246 * Getter for the description 247 * 248 * @return string 249 */ 250 public function getDescription() 251 { 252 return $this->description; 253 } 254 255 /** 256 * Setter for the title 257 * 258 * @param string $title 259 */ 260 public function setTitle($title) 261 { 262 $this->title = $title; 263 } 264 265 /** 266 * Setter for the description 267 * 268 * @param string $desc 269 */ 270 public function setDescription($desc) 271 { 272 $this->description = $desc; 273 } 274 275 /** 276 * Setter for the name of the data-source table 277 * 278 * @return string 279 */ 280 public function getItemTableName() 281 { 282 return $this->itemTableName; 283 } 284 285 /** 286 * Setter for the name of the data-source table 287 * 288 * @param string $tableName 289 */ 290 public function setItemTableName($tableName) 291 { 292 $this->itemTableName = $tableName; 293 } 294 295 /** 296 * Sorts collection via given callBackFunction 297 * 298 * The comparison function given as must return an integer less than, equal to, or greater than 299 * zero if the first argument is considered to be respectively less than, equal to, or greater than the second. 300 * 301 * @param callable $callbackFunction 302 * @see http://www.php.net/manual/en/function.usort.php 303 */ 304 public function usort($callbackFunction) 305 { 306 // @todo Implement usort() method with TCEforms in mind 307 throw new \RuntimeException('This method is not yet supported.', 1322545589); 308 } 309 310 /** 311 * Moves the item within the collection 312 * 313 * the item at $currentPosition will be moved to 314 * $newPosition. Omitting $newPosition will move to top. 315 * 316 * @param int $currentPosition 317 * @param int $newPosition 318 */ 319 public function moveItemAt($currentPosition, $newPosition = 0) 320 { 321 // @todo Implement usort() method with TCEforms in mind 322 throw new \RuntimeException('This method is not yet supported.', 1322545626); 323 } 324 325 /** 326 * Returns the uid of the collection 327 * 328 * @return int 329 */ 330 public function getIdentifier() 331 { 332 return $this->uid; 333 } 334 335 /** 336 * Sets the identifier of the collection 337 * 338 * @param int $id 339 */ 340 public function setIdentifier($id) 341 { 342 $this->uid = (int)$id; 343 } 344 345 /** 346 * Loads the collections with the given id from persistence 347 * 348 * For memory reasons, per default only f.e. title, database-table, 349 * identifier (what ever static data is defined) is loaded. 350 * Entries can be load on first access. 351 * 352 * @param int $id Id of database record to be loaded 353 * @param bool $fillItems Populates the entries directly on load, might be bad for memory on large collections 354 * @return CollectionInterface 355 */ 356 public static function load($id, $fillItems = false) 357 { 358 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(static::getCollectionDatabaseTable()); 359 $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 360 $collectionRecord = $queryBuilder->select('*') 361 ->from(static::getCollectionDatabaseTable()) 362 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))) 363 ->executeQuery() 364 ->fetchAssociative(); 365 return self::create($collectionRecord ?: [], $fillItems); 366 } 367 368 /** 369 * Creates a new collection objects and reconstitutes the 370 * given database record to the new object. 371 * 372 * @param array $collectionRecord Database record 373 * @param bool $fillItems Populates the entries directly on load, might be bad for memory on large collections 374 * @return CollectionInterface 375 */ 376 public static function create(array $collectionRecord, $fillItems = false) 377 { 378 // [phpstan] Unsafe usage of new static() 379 // todo: Either mark this class or its constructor final or use new self instead. 380 $collection = new static(); 381 $collection->fromArray($collectionRecord); 382 if ($fillItems) { 383 $collection->loadContents(); 384 } 385 return $collection; 386 } 387 388 /** 389 * Persists current collection state to underlying storage 390 */ 391 public function persist() 392 { 393 $uid = $this->getIdentifier() == 0 ? 'NEW' . random_int(100000, 999999) : $this->getIdentifier(); 394 $data = [ 395 trim(static::getCollectionDatabaseTable()) => [ 396 $uid => $this->getPersistableDataArray(), 397 ], 398 ]; 399 // New records always must have a pid 400 if ($this->getIdentifier() == 0) { 401 $data[trim(static::getCollectionDatabaseTable())][$uid]['pid'] = 0; 402 } 403 /** @var \TYPO3\CMS\Core\DataHandling\DataHandler $tce */ 404 $tce = GeneralUtility::makeInstance(DataHandler::class); 405 $tce->start($data, []); 406 $tce->process_datamap(); 407 } 408 409 /** 410 * Returns an array of the persistable properties and contents 411 * which are processable by DataHandler. 412 * 413 * For internal usage in persist only. 414 * 415 * @return array 416 */ 417 abstract protected function getPersistableDataArray(); 418 419 /** 420 * Generates comma-separated list of entry uids for usage in DataHandler 421 * 422 * also allow to add table name, if it might be needed by DataHandler for 423 * storing the relation 424 * 425 * @param bool $includeTableName 426 * @return string 427 */ 428 protected function getItemUidList($includeTableName = true) 429 { 430 $list = []; 431 foreach ($this->storage as $entry) { 432 $list[] = ($includeTableName ? $this->getItemTableName() . '_' : '') . $entry['uid']; 433 } 434 return implode(',', $list); 435 } 436 437 /** 438 * Builds an array representation of this collection 439 * 440 * @return array 441 */ 442 public function toArray() 443 { 444 $itemArray = []; 445 foreach ($this->storage as $item) { 446 $itemArray[] = $item; 447 } 448 return [ 449 'uid' => $this->getIdentifier(), 450 'title' => $this->getTitle(), 451 'description' => $this->getDescription(), 452 'table_name' => $this->getItemTableName(), 453 'items' => $itemArray, 454 ]; 455 } 456 457 /** 458 * Loads the properties of this collection from an array 459 * 460 * @param array $array 461 */ 462 public function fromArray(array $array) 463 { 464 $this->uid = $array['uid']; 465 $this->title = $array['title']; 466 $this->description = $array['description']; 467 $this->itemTableName = $array['table_name']; 468 } 469 470 protected static function getCollectionDatabaseTable(): string 471 { 472 if (!empty(static::$storageTableName)) { 473 return static::$storageTableName; 474 } 475 throw new \RuntimeException('No storage table name was defined the class "' . static::class . '".', 1592207959); 476 } 477} 478