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