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