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\Category\Collection; 17 18use TYPO3\CMS\Core\Collection\AbstractRecordCollection; 19use TYPO3\CMS\Core\Collection\CollectionInterface; 20use TYPO3\CMS\Core\Collection\EditableCollectionInterface; 21use TYPO3\CMS\Core\Database\ConnectionPool; 22use TYPO3\CMS\Core\Database\Query\QueryBuilder; 23use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; 24use TYPO3\CMS\Core\Utility\GeneralUtility; 25 26/** 27 * Category Collection to handle records attached to a category 28 */ 29class CategoryCollection extends AbstractRecordCollection implements EditableCollectionInterface 30{ 31 /** 32 * The table name collections are stored to 33 * 34 * @var string 35 */ 36 protected static $storageTableName = 'sys_category'; 37 38 /** 39 * Name of the categories-relation field (used in the MM_match_fields/fieldname property of the TCA) 40 * 41 * @var string 42 */ 43 protected $relationFieldName = 'categories'; 44 45 /** 46 * Creates this object. 47 * 48 * @param string $tableName Name of the table to be working on 49 * @param string $fieldName Name of the field where the categories relations are defined 50 * @throws \RuntimeException 51 */ 52 public function __construct($tableName = null, $fieldName = null) 53 { 54 parent::__construct(); 55 if (!empty($tableName)) { 56 $this->setItemTableName($tableName); 57 } elseif (empty($this->itemTableName)) { 58 throw new \RuntimeException(self::class . ' needs a valid itemTableName.', 1341826168); 59 } 60 if (!empty($fieldName)) { 61 $this->setRelationFieldName($fieldName); 62 } 63 } 64 65 /** 66 * Creates a new collection objects and reconstitutes the 67 * given database record to the new object. 68 * 69 * @param array $collectionRecord Database record 70 * @param bool $fillItems Populates the entries directly on load, might be bad for memory on large collections 71 * @return CategoryCollection 72 */ 73 public static function create(array $collectionRecord, $fillItems = false) 74 { 75 /** @var CategoryCollection $collection */ 76 $collection = GeneralUtility::makeInstance( 77 self::class, 78 $collectionRecord['table_name'], 79 $collectionRecord['field_name'] 80 ); 81 $collection->fromArray($collectionRecord); 82 if ($fillItems) { 83 $collection->loadContents(); 84 } 85 return $collection; 86 } 87 88 /** 89 * Loads the collections with the given id from persistence 90 * For memory reasons, per default only f.e. title, database-table, 91 * identifier (what ever static data is defined) is loaded. 92 * Entries can be load on first access. 93 * 94 * @param int $id Id of database record to be loaded 95 * @param bool $fillItems Populates the entries directly on load, might be bad for memory on large collections 96 * @param string $tableName Name of table from which entries should be loaded 97 * @param string $fieldName Name of the categories relation field 98 * @return CollectionInterface 99 */ 100 public static function load($id, $fillItems = false, $tableName = '', $fieldName = '') 101 { 102 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 103 ->getQueryBuilderForTable(static::$storageTableName); 104 105 $queryBuilder->getRestrictions() 106 ->removeAll() 107 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 108 109 $collectionRecord = $queryBuilder->select('*') 110 ->from(static::$storageTableName) 111 ->where( 112 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)) 113 ) 114 ->setMaxResults(1) 115 ->execute() 116 ->fetch(); 117 118 $collectionRecord['table_name'] = $tableName; 119 $collectionRecord['field_name'] = $fieldName; 120 121 return self::create($collectionRecord, $fillItems); 122 } 123 124 /** 125 * Selects the collected records in this collection, by 126 * looking up the MM relations of this record to the 127 * table name defined in the local field 'table_name'. 128 * 129 * @return QueryBuilder 130 */ 131 protected function getCollectedRecordsQueryBuilder() 132 { 133 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 134 ->getQueryBuilderForTable(static::$storageTableName); 135 $queryBuilder->getRestrictions()->removeAll(); 136 137 $queryBuilder->select($this->getItemTableName() . '.*') 138 ->from(static::$storageTableName) 139 ->join( 140 static::$storageTableName, 141 'sys_category_record_mm', 142 'sys_category_record_mm', 143 $queryBuilder->expr()->eq( 144 'sys_category_record_mm.uid_local', 145 $queryBuilder->quoteIdentifier(static::$storageTableName . '.uid') 146 ) 147 ) 148 ->join( 149 'sys_category_record_mm', 150 $this->getItemTableName(), 151 $this->getItemTableName(), 152 $queryBuilder->expr()->eq( 153 'sys_category_record_mm.uid_foreign', 154 $queryBuilder->quoteIdentifier($this->getItemTableName() . '.uid') 155 ) 156 ) 157 ->where( 158 $queryBuilder->expr()->eq( 159 static::$storageTableName . '.uid', 160 $queryBuilder->createNamedParameter($this->getIdentifier(), \PDO::PARAM_INT) 161 ), 162 $queryBuilder->expr()->eq( 163 'sys_category_record_mm.tablenames', 164 $queryBuilder->createNamedParameter($this->getItemTableName(), \PDO::PARAM_STR) 165 ), 166 $queryBuilder->expr()->eq( 167 'sys_category_record_mm.fieldname', 168 $queryBuilder->createNamedParameter($this->getRelationFieldName(), \PDO::PARAM_STR) 169 ) 170 ) 171 // @todo: MM TCA ref docs say 'sorting' is a required field for MM relations, but it seems code always 172 // hard codes to field name 'sorting' (RelationHandler) and never uses 'sorting' TCA definition. 173 // Furthermore through implementing auto creation of MM tables from TCA 'sorting' is auto created. 174 // Add required sorting field. 175 ->orderBy('sys_category_record_mm.sorting', 'ASC') 176 // Add foreign uid field to ensure determistic sorting across dbms and dbms versions 177 ->addOrderBy('sys_category_record_mm.uid_foreign', 'ASC') 178 ; 179 180 return $queryBuilder; 181 } 182 183 /** 184 * Gets the collected records in this collection, by 185 * using <getCollectedRecordsQueryBuilder>. 186 * 187 * @return array 188 */ 189 protected function getCollectedRecords() 190 { 191 $relatedRecords = []; 192 193 $queryBuilder = $this->getCollectedRecordsQueryBuilder(); 194 $result = $queryBuilder->execute(); 195 196 while ($record = $result->fetch()) { 197 $relatedRecords[] = $record; 198 } 199 200 return $relatedRecords; 201 } 202 203 /** 204 * Populates the content-entries of the storage 205 * Queries the underlying storage for entries of the collection 206 * and adds them to the collection data. 207 * If the content entries of the storage had not been loaded on creation 208 * ($fillItems = false) this function is to be used for loading the contents 209 * afterwards. 210 */ 211 public function loadContents() 212 { 213 $entries = $this->getCollectedRecords(); 214 $this->removeAll(); 215 foreach ($entries as $entry) { 216 $this->add($entry); 217 } 218 } 219 220 /** 221 * Returns an array of the persistable properties and contents 222 * which are processable by DataHandler. 223 * for internal usage in persist only. 224 * 225 * @return array 226 */ 227 protected function getPersistableDataArray() 228 { 229 return [ 230 'title' => $this->getTitle(), 231 'description' => $this->getDescription(), 232 'items' => $this->getItemUidList(true) 233 ]; 234 } 235 236 /** 237 * Adds on entry to the collection 238 * 239 * @param mixed $data 240 */ 241 public function add($data) 242 { 243 $this->storage->push($data); 244 } 245 246 /** 247 * Adds a set of entries to the collection 248 * 249 * @param CollectionInterface $other 250 */ 251 public function addAll(CollectionInterface $other) 252 { 253 foreach ($other as $value) { 254 $this->add($value); 255 } 256 } 257 258 /** 259 * Removes the given entry from collection 260 * Note: not the given "index" 261 * 262 * @param mixed $data 263 */ 264 public function remove($data) 265 { 266 $offset = 0; 267 foreach ($this->storage as $value) { 268 if ($value == $data) { 269 break; 270 } 271 $offset++; 272 } 273 $this->storage->offsetUnset($offset); 274 } 275 276 /** 277 * Removes all entries from the collection 278 * collection will be empty afterwards 279 */ 280 public function removeAll() 281 { 282 $this->storage = new \SplDoublyLinkedList(); 283 } 284 285 /** 286 * Gets the current available items. 287 * 288 * @return array 289 */ 290 public function getItems() 291 { 292 $itemArray = []; 293 /** @var \TYPO3\CMS\Core\Resource\File $item */ 294 foreach ($this->storage as $item) { 295 $itemArray[] = $item; 296 } 297 return $itemArray; 298 } 299 300 /** 301 * Sets the name of the categories relation field 302 * 303 * @param string $field 304 */ 305 public function setRelationFieldName($field) 306 { 307 $this->relationFieldName = $field; 308 } 309 310 /** 311 * Gets the name of the categories relation field 312 * 313 * @return string 314 */ 315 public function getRelationFieldName() 316 { 317 return $this->relationFieldName; 318 } 319 320 /** 321 * Getter for the storage table name 322 * 323 * @return string 324 */ 325 public static function getStorageTableName() 326 { 327 return self::$storageTableName; 328 } 329 330 /** 331 * Getter for the storage items field 332 * 333 * @return string 334 */ 335 public static function getStorageItemsField() 336 { 337 return self::$storageItemsField; 338 } 339} 340