[FEATURE] Introduce Data Processor for Files 08/40508/10
authorBenjamin Mack <benni@typo3.org>
Sun, 21 Jun 2015 13:45:34 +0000 (15:45 +0200)
committerAndreas Fernandez <typo3@scripting-base.de>
Wed, 8 Jul 2015 06:39:56 +0000 (08:39 +0200)
In order to use real file objects within Fluid templates or other
places with cObjects, a new data processor for Files is added.

As it does most of the same part (collecting files from various
sources) as the FILES cObject, the logic has been separated
into an own class, the so-called "File Collector".

Resolves: #67662
Releases: master
Change-Id: I05c494db65caa7efd1847504a057b3b21efb8702
Reviewed-on: http://review.typo3.org/40508
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Frans Saris <franssaris@gmail.com>
Tested-by: Frans Saris <franssaris@gmail.com>
Reviewed-by: Andreas Fernandez <typo3@scripting-base.de>
Tested-by: Andreas Fernandez <typo3@scripting-base.de>
typo3/sysext/core/Documentation/Changelog/master/Feature-67662-DataProcessorForFiles.rst [new file with mode: 0644]
typo3/sysext/frontend/Classes/ContentObject/FilesContentObject.php
typo3/sysext/frontend/Classes/DataProcessing/FilesProcessor.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Resource/FileCollector.php [new file with mode: 0644]
typo3/sysext/frontend/Tests/Unit/ContentObject/FilesContentObjectTest.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-67662-DataProcessorForFiles.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-67662-DataProcessorForFiles.rst
new file mode 100644 (file)
index 0000000..d1309bc
--- /dev/null
@@ -0,0 +1,60 @@
+=========================================
+Feature: #67662 - DataProcessor for files
+=========================================
+
+Description
+===========
+
+A new Files DataProcessor has been introduced, which can be used to prepare data to be handled by a ContentObject
+implementing the processors, e.g. the FLUIDTEMPLATE ContentObject. The FilesProcessor resolves File References, Files,
+or Files inside a folder or collection to be used for output in the Frontend. A FLUIDTEMPLATE can then simply iterate
+over processed data automatically.
+
+
+.. code-block:: typoscript
+
+       tt_content.image.20 = FLUIDTEMPLATE
+       tt_content.image.20.file = EXT:myextension/Resources/Private/Templates/ContentObjects/Image.html
+       tt_content.image.20.dataProcessing.10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
+
+       # the field name where relations are set
+       # + stdWrap
+       tt_content.image.20.dataProcessing.10.references.fieldName = image
+
+       # the table name where relations are put, defaults to the currently selected record from $cObj->getTable()
+       # + stdWrap
+       tt_content.image.20.dataProcessing.10.references.table = tt_content
+
+       # A list of sys_file UID records
+       # + stdWrap
+       tt_content.image.20.dataProcessing.10.files = 21,42
+
+       # A list of File Collection UID records
+       # + stdWrap
+       tt_content.image.20.dataProcessing.10.collections = 13,14
+
+       # A list of FAL Folder identifiers
+       # + stdWrap
+       tt_content.image.20.dataProcessing.10.folders = 1:introduction/images/,1:introduction/posters/
+
+       # Property of which the files should be sorted after they have been accumulated + stdWrap
+       # can be any property of sys_file, sys_file_metadata
+       tt_content.image.20.dataProcessing.10.sorting = description
+
+       # Can be "ascending", "descending" or "random", defaults to "ascending" if none given + stdWrap
+       tt_content.image.20.dataProcessing.10.sorting.direction = descending
+
+       # The target variable to be handed to the ContentObject again, can be used
+       # in Fluid e.g. to iterate over the objects. defaults to "files" when non given
+       # + stdWrap
+       tt_content.image.20.dataProcessing.10.as = myfiles
+
+
+In the Fluid template then iterate over the files:
+
+.. code-block:: html
+       <ul>
+       <f:for each="{myfiles}" as="file">
+               <li><a href="{file.publicUrl}">{file.name}</a></li>
+       </f:for>
+       </ul>
index e5c5f2c..6732116 100644 (file)
@@ -14,16 +14,10 @@ namespace TYPO3\CMS\Frontend\ContentObject;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Resource\Collection\AbstractFileCollection;
 use TYPO3\CMS\Core\Resource\Exception;
-use TYPO3\CMS\Core\Resource\FileCollectionRepository;
-use TYPO3\CMS\Core\Resource\FileInterface;
-use TYPO3\CMS\Core\Resource\FileRepository;
-use TYPO3\CMS\Core\Resource\Folder;
-use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
-use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Frontend\Resource\FileCollector;
 
 /**
  * Contains FILES content object
@@ -33,21 +27,6 @@ use TYPO3\CMS\Core\Log\LogManager;
 class FilesContentObject extends AbstractContentObject {
 
        /**
-        * @var FileCollectionRepository|NULL
-        */
-       protected $collectionRepository = NULL;
-
-       /**
-        * @var ResourceFactory|NULL
-        */
-       protected $fileFactory = NULL;
-
-       /**
-        * @var FileRepository|NULL
-        */
-       protected $fileRepository = NULL;
-
-       /**
         * Rendering the cObject FILES
         *
         * @param array $conf Array of TypoScript properties
@@ -58,115 +37,13 @@ class FilesContentObject extends AbstractContentObject {
                        return '';
                }
 
-               $fileObjects = array();
-               // Getting the files
-               if ($conf['references'] || $conf['references.']) {
-                       /*
-                       The TypoScript could look like this:# all items related to the page.media field:
-                       references {
-                       table = pages
-                       uid.data = page:uid
-                       fieldName = media
-                       }# or: sys_file_references with uid 27:
-                       references = 27
-                        */
-                       $referencesUid = $this->cObj->stdWrapValue('references', $conf);
-                       $referencesUidArray = GeneralUtility::intExplode(',', $referencesUid, TRUE);
-                       foreach ($referencesUidArray as $referenceUid) {
-                               try {
-                                       $this->addToArray(
-                                               $this->getFileFactory()->getFileReferenceObject($referenceUid),
-                                               $fileObjects
-                                       );
-                               } catch (Exception $e) {
-                                       /** @var \TYPO3\CMS\Core\Log\Logger $logger */
-                                       $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
-                                       $logger->warning('The file-reference with uid  "' . $referenceUid . '" could not be found and won\'t be included in frontend output');
-                               }
-                       }
-
-                       $this->handleFileReferences($conf, (array)$this->cObj->data, $fileObjects);
-               }
-               if ($conf['files'] || $conf['files.']) {
-                       /*
-                       The TypoScript could look like this:
-                       # with sys_file UIDs:
-                       files = 12,14,15# using stdWrap:
-                       files.field = some_field
-                        */
-                       $fileUids = GeneralUtility::intExplode(',', $this->cObj->stdWrapValue('files', $conf), TRUE);
-                       foreach ($fileUids as $fileUid) {
-                               try {
-                                       $this->addToArray($this->getFileFactory()->getFileObject($fileUid), $fileObjects);
-                               } catch (Exception $e) {
-                                       /** @var \TYPO3\CMS\Core\Log\Logger $logger */
-                                       $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
-                                       $logger->warning('The file with uid  "' . $fileUid . '" could not be found and won\'t be included in frontend output');
-                               }
-                       }
-               }
-               if ($conf['collections'] || $conf['collections.']) {
-                       $collectionUids = GeneralUtility::intExplode(',', $this->cObj->stdWrapValue('collections', $conf), TRUE);
-                       foreach ($collectionUids as $collectionUid) {
-                               try {
-                                       $fileCollection = $this->getCollectionRepository()->findByUid($collectionUid);
-                                       if ($fileCollection instanceof AbstractFileCollection) {
-                                               $fileCollection->loadContents();
-                                               $this->addToArray($fileCollection->getItems(), $fileObjects);
-                                       }
-                               } catch (Exception $e) {
-                                       /** @var \TYPO3\CMS\Core\Log\Logger $logger */
-                                       $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
-                                       $logger->warning('The file-collection with uid  "' . $collectionUid . '" could not be found or contents could not be loaded and won\'t be included in frontend output');
-                               }
-                       }
-               }
-               if ($conf['folders'] || $conf['folders.']) {
-                       $folderIdentifiers = GeneralUtility::trimExplode(',', $this->cObj->stdWrapValue('folders', $conf));
-                       foreach ($folderIdentifiers as $folderIdentifier) {
-                               if ($folderIdentifier) {
-                                       try {
-                                               $folder = $this->getFileFactory()->getFolderObjectFromCombinedIdentifier($folderIdentifier);
-                                               if ($folder instanceof Folder) {
-                                                       $this->addToArray(array_values($folder->getFiles()), $fileObjects);
-                                               }
-                                       } catch (Exception $e) {
-                                               /** @var \TYPO3\CMS\Core\Log\Logger $logger */
-                                               $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
-                                               $logger->warning('The folder with identifier  "' . $folderIdentifier . '" could not be found and won\'t be included in frontend output');
-                                       }
-                               }
-                       }
-               }
-               // Rendering the files
-               $content = '';
-               // optionSplit applied to conf to allow differnt settings per file
-               $splitConf = $GLOBALS['TSFE']->tmpl->splitConfArray($conf, count($fileObjects));
-
-               // Enable sorting for multiple fileObjects
-               $sortingProperty = '';
-               if ($conf['sorting'] || $conf['sorting.']) {
-                       $sortingProperty = $this->cObj->stdWrapValue('sorting', $conf);
-               }
-               if ($sortingProperty !== '' && count($fileObjects) > 1) {
-                       @usort($fileObjects, function(FileInterface $a, FileInterface $b) use($sortingProperty) {
-                               if ($a->hasProperty($sortingProperty) && $b->hasProperty($sortingProperty)) {
-                                       return strnatcasecmp($a->getProperty($sortingProperty), $b->getProperty($sortingProperty));
-                               } else {
-                                       return 0;
-                               }
-                       });
-                       $sortingDirection = isset($conf['sorting.']['direction']) ? $conf['sorting.']['direction'] : '';
-                       if (isset($conf['sorting.']['direction.'])) {
-                               $sortingDirection = $this->cObj->stdWrap($sortingDirection, $conf['sorting.']['direction.']);
-                       }
-                       if (strtolower($sortingDirection) === 'desc') {
-                               $fileObjects = array_reverse($fileObjects);
-                       }
-               }
-
+               $fileCollector = $this->findAndSortFiles($conf);
+               $fileObjects = $fileCollector->getFiles();
                $availableFileObjectCount = count($fileObjects);
 
+               // optionSplit applied to conf to allow different settings per file
+               $splitConf = $GLOBALS['TSFE']->tmpl->splitConfArray($conf, $availableFileObjectCount);
+
                $start = 0;
                if (!empty($conf['begin'])) {
                        $start = (int)$conf['begin'];
@@ -189,6 +66,8 @@ class FilesContentObject extends AbstractContentObject {
                $GLOBALS['TSFE']->register['FILES_COUNT'] = min($limit, $availableFileObjectCount);
                $fileObjectCounter = 0;
                $keys = array_keys($fileObjects);
+
+               $content = '';
                for ($i = $start; $i < $end; $i++) {
                        $key = $keys[$i];
                        $fileObject = $fileObjects[$key];
@@ -198,77 +77,78 @@ class FilesContentObject extends AbstractContentObject {
                        $content .= $this->cObj->cObjGetSingle($splitConf[$key]['renderObj'], $splitConf[$key]['renderObj.']);
                        $fileObjectCounter++;
                }
-               $content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
-               return $content;
-       }
 
-       /**
-        * Sets the file factory.
-        *
-        * @param ResourceFactory $fileFactory
-        * @return void
-        */
-       public function setFileFactory($fileFactory) {
-               $this->fileFactory = $fileFactory;
+               return $this->cObj->stdWrap($content, $conf['stdWrap.']);
        }
 
        /**
-        * Returns the file factory.
+        * Function to check for references, collections, folders and
+        * accumulates into one etc.
         *
-        * @return ResourceFactory
+        * @param array $conf
+        * @return FileCollector
         */
-       public function getFileFactory() {
-               if ($this->fileFactory === NULL) {
-                       $this->fileFactory = GeneralUtility::makeInstance(ResourceFactory::class);
-               }
+       protected function findAndSortFiles(array $conf) {
 
-               return $this->fileFactory;
-       }
+               $fileCollector = $this->getFileCollector();
 
-       /**
-        * Sets the file repository.
-        *
-        * @param FileRepository $fileRepository
-        * @return void
-        */
-       public function setFileRepository($fileRepository) {
-               $this->fileRepository = $fileRepository;
-       }
+               // Getting the files
+               if ($conf['references'] || $conf['references.']) {
+                       /*
+                       The TypoScript could look like this:
+                       # all items related to the page.media field:
+                       references {
+                               table = pages
+                               uid.data = page:uid
+                               fieldName = media
+                       }
+                       # or: sys_file_references with uid 27:
+                       references = 27
+                        */
+                       $referencesUidList = $this->cObj->stdWrapValue('references', $conf);
+                       $referencesUids = GeneralUtility::intExplode(',', $referencesUidList, TRUE);
+                       $fileCollector->addFileReferences($referencesUids);
 
-       /**
-        * Returns the file repository.
-        *
-        * @return FileRepository
-        */
-       public function getFileRepository() {
-               if ($this->fileRepository === NULL) {
-                       $this->fileRepository = GeneralUtility::makeInstance(FileRepository::class);
+                       if (!empty($conf['references.'])) {
+                               $this->addFileReferences($conf, (array)$this->cObj->data, $fileCollector);
+                       }
                }
 
-               return $this->fileRepository;
-       }
+               if ($conf['files'] || $conf['files.']) {
+                       /*
+                       The TypoScript could look like this:
+                       # with sys_file UIDs:
+                       files = 12,14,15# using stdWrap:
+                       files.field = some_field
+                        */
+                       $fileUids = GeneralUtility::intExplode(',', $this->cObj->stdWrapValue('files', $conf), TRUE);
+                       $fileCollector->addFiles($fileUids);
+               }
 
-       /**
-        * Sets the collection repository.
-        *
-        * @param FileCollectionRepository $collectionRepository
-        * @return void
-        */
-       public function setCollectionRepository($collectionRepository) {
-               $this->collectionRepository = $collectionRepository;
-       }
+               if ($conf['collections'] || $conf['collections.']) {
+                       $collectionUids = GeneralUtility::intExplode(',', $this->cObj->stdWrapValue('collections', $conf), TRUE);
+                       $fileCollector->addFilesFromFileCollections($collectionUids);
+               }
 
-       /**
-        * Returns the collection repository.
-        *
-        * @return FileCollectionRepository
-        */
-       public function getCollectionRepository() {
-               if ($this->collectionRepository === NULL) {
-                       $this->collectionRepository = GeneralUtility::makeInstance(FileCollectionRepository::class);
+               if ($conf['folders'] || $conf['folders.']) {
+                       $folderIdentifiers = GeneralUtility::trimExplode(',', $this->cObj->stdWrapValue('folders', $conf));
+                       $fileCollector->addFilesFromFolders($folderIdentifiers);
+               }
+
+               // Enable sorting for multiple fileObjects
+               $sortingProperty = '';
+               if ($conf['sorting'] || $conf['sorting.']) {
+                       $sortingProperty = $this->cObj->stdWrapValue('sorting', $conf);
+               }
+               if ($sortingProperty !== '') {
+                       $sortingDirection = isset($conf['sorting.']['direction']) ? $conf['sorting.']['direction'] : '';
+                       if (isset($conf['sorting.']['direction.'])) {
+                               $sortingDirection = $this->cObj->stdWrap($sortingDirection, $conf['sorting.']['direction.']);
+                       }
+                       $fileCollector->sort($sortingProperty, $sortingDirection);
                }
 
-               return $this->collectionRepository;
+               return $fileCollector;
        }
 
        /**
@@ -276,13 +156,10 @@ class FilesContentObject extends AbstractContentObject {
         *
         * @param array $configuration TypoScript configuration
         * @param array $element The parent element referencing to files
-        * @param array $fileObjects Collection of file objects
-        * @return void
+        * @param FileCollector $fileCollector
+        * @return array
         */
-       protected function handleFileReferences(array $configuration, array $element, array &$fileObjects) {
-               if (empty($configuration['references.'])) {
-                       return;
-               }
+       protected function addFileReferences(array $configuration, array $element, FileCollector $fileCollector) {
 
                // It's important that this always stays "fieldName" and not be renamed to "field" as it would otherwise collide with the stdWrap key of that name
                $referencesFieldName = $this->cObj->stdWrapValue('fieldName', $configuration['references.']);
@@ -323,26 +200,7 @@ class FilesContentObject extends AbstractContentObject {
                }
 
                if (is_array($element)) {
-                       $references = $pageRepository->getFileReferences(
-                               $referencesForeignTable,
-                               $referencesFieldName,
-                               $element
-                       );
-                       $this->addToArray($references, $fileObjects);
-               }
-       }
-
-       /**
-        * Adds $newItems to $theArray, which is passed by reference. Array must only consist of numerical keys.
-        *
-        * @param mixed $newItems Array with new items or single object that's added.
-        * @param array $theArray The array the new items should be added to. Must only contain numeric keys (for array_merge() to add items instead of replacing).
-        */
-       protected function addToArray($newItems, array &$theArray) {
-               if (is_array($newItems)) {
-                       $theArray = array_merge($theArray, $newItems);
-               } elseif (is_object($newItems)) {
-                       $theArray[] = $newItems;
+                       $fileCollector->addFilesFromRelation($referencesForeignTable, $referencesFieldName, $element);
                }
        }
 
@@ -353,4 +211,10 @@ class FilesContentObject extends AbstractContentObject {
                return $GLOBALS['TSFE']->sys_page;
        }
 
+       /**
+        * @return FileCollector
+        */
+       protected function getFileCollector() {
+               return GeneralUtility::makeInstance(FileCollector::class);
+       }
 }
diff --git a/typo3/sysext/frontend/Classes/DataProcessing/FilesProcessor.php b/typo3/sysext/frontend/Classes/DataProcessing/FilesProcessor.php
new file mode 100644 (file)
index 0000000..c2d8678
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+namespace TYPO3\CMS\Frontend\DataProcessing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
+use TYPO3\CMS\Frontend\Resource\FileCollector;
+
+/**
+ * This data processor can be used for processing data for record which contain
+ * relations to sys_file records (e.g. sys_file_reference records) or for fetching
+ * files directly from UIDs or from folders or collections.
+ *
+ *
+ * Example TypoScript configuration:
+ *
+ * 10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
+ * 10 {
+ *   references.fieldName = image
+ *   collections = 13,15
+ *   as = myfiles
+ * }
+ *
+ * whereas "myfiles" can further be used as a variable {myfiles} inside a Fluid template for iteration.
+ */
+class FilesProcessor implements DataProcessorInterface {
+
+       /**
+        * Process data of a record to resolve File objects to the view
+        *
+        * @param ContentObjectRenderer $cObj The data of the content element or page
+        * @param array $contentObjectConfiguration The configuration of Content Object
+        * @param array $processorConfiguration The configuration of this processor
+        * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
+        * @return array the processed data as key/value store
+        */
+       public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration,  array $processedData) {
+
+               if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
+                       return $processedData;
+               }
+
+               // gather data
+               /** @var FileCollector $fileCollector */
+               $fileCollector = GeneralUtility::makeInstance(FileCollector::class);
+
+               // references / relations
+               if (!empty($processorConfiguration['references.'])) {
+                       $referenceConfiguration = $processorConfiguration['references.'];
+                       $relationField = $cObj->stdWrapValue('fieldName', $referenceConfiguration);
+
+                       // If no reference fieldName is set, there's nothing to do
+                       if (!empty($relationField)) {
+                               // Fetch the references of the default element
+                               $relationTable = $cObj->stdWrapValue('table', $referenceConfiguration, $cObj->getCurrentTable());
+                               if (!empty($relationTable)) {
+                                       $fileCollector->addFilesFromRelation($relationTable, $relationField, $cObj->data);
+                               }
+                       }
+               }
+
+               // files
+               $files = $cObj->stdWrapValue('files', $processorConfiguration);
+               if ($files) {
+                       $files = GeneralUtility::intExplode(',', $files, TRUE);
+                       $fileCollector->addFiles($files);
+               }
+
+               // collections
+               $collections = $cObj->stdWrapValue('collections', $processorConfiguration);
+               if (!empty($collections)) {
+                       $collections = GeneralUtility::trimExplode(',', $collections, TRUE);
+                       $fileCollector->addFilesFromFileCollections($collections);
+               }
+
+               // folders
+               $folders = $cObj->stdWrapValue('folders', $processorConfiguration);
+               if (!empty($folders)) {
+                       $folders = GeneralUtility::trimExplode(',', $folders, TRUE);
+                       $fileCollector->addFilesFromFolders($folders);
+               }
+
+               // make sure to sort the files
+               $sortingProperty = $cObj->stdWrapValue('sorting', $processorConfiguration);
+               if ($sortingProperty) {
+                       $sortingDirection = $cObj->stdWrapValue(
+                               'direction',
+                               isset($processorConfiguration['sorting.']) ? $processorConfiguration['sorting.'] : array(),
+                               'ascending'
+                       );
+
+                       $fileCollector->sort($sortingProperty, $sortingDirection);
+               }
+
+               // set the files into a variable, default "files"
+               $targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'files');
+               $processedData[$targetVariableName] = $fileCollector->getFiles();
+
+               return $processedData;
+       }
+}
diff --git a/typo3/sysext/frontend/Classes/Resource/FileCollector.php b/typo3/sysext/frontend/Classes/Resource/FileCollector.php
new file mode 100644 (file)
index 0000000..3d46ed3
--- /dev/null
@@ -0,0 +1,307 @@
+<?php
+namespace TYPO3\CMS\Frontend\Resource;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\Resource\Exception;
+use TYPO3\CMS\Core\Resource\FileCollectionRepository;
+use TYPO3\CMS\Core\Resource\FileInterface;
+use TYPO3\CMS\Core\Resource\FileRepository;
+use TYPO3\CMS\Core\Resource\Folder;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Object to collect files from various sources during runtime
+ * Sources can be file references, file collections or folders
+ *
+ * Use in FILES Content Object or for a Fluid Data Processor
+ *
+ * Is not persisted, use only in FE.
+ */
+class FileCollector implements \Countable {
+
+       /**
+        * The files
+        *
+        * @var array
+        */
+       protected $files = array();
+
+       /**
+        * The file repository
+        *
+        * @var \TYPO3\CMS\Core\Resource\FileRepository
+        */
+       protected $fileRepository;
+
+       /**
+        * The file collection repository
+        *
+        * @var \TYPO3\CMS\Core\Resource\FileCollectionRepository
+        */
+       protected $fileCollectionRepository;
+
+       /**
+        * The resource factory
+        *
+        * @var \TYPO3\CMS\Core\Resource\ResourceFactory
+        */
+       protected $resourceFactory;
+
+       /**
+        * Add files
+        *
+        * @param array $fileUids
+        */
+       public function addFiles($fileUids = array()) {
+               if (!empty($fileUids)) {
+                       foreach ($fileUids as $fileUid) {
+                               try {
+                                       $this->addFileObject($this->getResourceFactory()->getFileObject($fileUid));
+                               } catch (Exception $e) {
+                                       $this->getLogger()->warning(
+                                               'The file with uid  "' . $fileUid
+                                               . '" could not be found and won\'t be included in frontend output',
+                                               array('exception' => $e)
+                                       );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Add files to the collection from a relation
+        *
+        * @param string $relationTable The table of the relation (e.g. tt_content or pages)
+        * @param string $relationField The field which holds the files (e.g. media or images)
+        * @param array $referenceRecord the record which is referencing the files
+        * @return void
+        */
+       public function addFilesFromRelation($relationTable, $relationField, $referenceRecord) {
+               if (is_object($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE']->sys_page)) {
+                       $fileReferences = $GLOBALS['TSFE']->sys_page->getFileReferences($relationTable, $relationField, $referenceRecord);
+               } else {
+                       $fileReferences = $this->getFileRepository()->findByRelation($relationTable, $relationField, $referenceRecord['uid']);
+               }
+
+               if (!empty($fileReferences)) {
+                       $this->addFileObjects($fileReferences);
+               }
+       }
+
+       /**
+        * Add files from UIDs of a reference
+        *
+        * @param array $fileReferenceUids
+        * @return void
+        */
+       public function addFileReferences($fileReferenceUids = array()) {
+               if (!empty($fileReferenceUids)) {
+                       foreach ($fileReferenceUids as $fileReferenceUid) {
+                               $fileObject = $this->getFileRepository()->findFileReferenceByUid($fileReferenceUid);
+                               $this->addFileObject($fileObject);
+                       }
+               }
+       }
+
+       /**
+        * Add files to the collection from multiple file collections
+        *
+        * @param array $fileCollectionUids The file collections uids
+        * @return void
+        */
+       public function addFilesFromFileCollections($fileCollectionUids = array()) {
+               if (!empty($fileCollectionUids)) {
+                       foreach ($fileCollectionUids as $fileCollectionUid) {
+                               $this->addFilesFromFileCollection($fileCollectionUid);
+                       }
+               }
+       }
+
+       /**
+        * Add files to the collection from one single file collection
+        *
+        * @param int $fileCollectionUid The file collections uid
+        * @return void
+        */
+       public function addFilesFromFileCollection($fileCollectionUid = NULL) {
+               if (!empty($fileCollectionUid)) {
+                       try {
+                               $fileCollection = $this->getFileCollectionRepository()->findByUid($fileCollectionUid);
+
+                               if ($fileCollection instanceof \TYPO3\CMS\Core\Resource\Collection\AbstractFileCollection) {
+                                       $fileCollection->loadContents();
+                                       $files = $fileCollection->getItems();
+
+                                       $this->addFileObjects($files);
+                               }
+                       } catch (Exception $e) {
+                               $this->getLogger()->warning(
+                                       'The file-collection with uid  "' . $fileCollectionUid
+                                       . '" could not be found or contents could not be loaded and won\'t be included in frontend output.',
+                                       array('exception' => $e)
+                               );
+                       }
+               }
+       }
+
+       /**
+        * Add files to the collection from multiple folders
+        *
+        * @param array $folderIdentifiers The folder identifiers
+        * @return void
+        */
+       public function addFilesFromFolders($folderIdentifiers = array()) {
+               if (!empty($folderIdentifiers)) {
+                       foreach ($folderIdentifiers as $folderIdentifier) {
+                               $this->addFilesFromFolder($folderIdentifier);
+                       }
+               }
+       }
+
+       /**
+        * Add files to the collection from one single folder
+        *
+        * @param string $folderIdentifier The folder identifier
+        */
+       public function addFilesFromFolder($folderIdentifier) {
+               if ($folderIdentifier) {
+                       try {
+                               $folder = $this->getResourceFactory()->getFolderObjectFromCombinedIdentifier($folderIdentifier);
+                               if ($folder instanceof Folder) {
+                                       $files = $folder->getFiles();
+
+                                       $this->addFileObjects($files);
+                               }
+                       } catch (Exception $e) {
+                               $this->getLogger()->warning(
+                                       'The folder with identifier  "' . $folderIdentifier
+                                       . '" could not be found and won\'t be included in frontend output',
+                                       array('exception' => $e)
+                               );
+                       }
+               }
+       }
+
+       /**
+        * Sort the file objects based on a property
+        *
+        * @param string $sortingProperty The sorting property
+        * @param string $sortingOrder can be ascending or descending or "random"
+        * @return void
+        */
+       public function sort($sortingProperty = '', $sortingOrder = 'ascending') {
+               if ($sortingProperty !== '' && count($this->files) > 1) {
+                       @usort(
+                               $this->files,
+                               function(
+                                       FileInterface $a,
+                                       FileInterface $b
+                               ) use($sortingProperty) {
+                                       if ($a->hasProperty($sortingProperty) && $b->hasProperty($sortingProperty)) {
+                                               return strnatcasecmp($a->getProperty($sortingProperty), $b->getProperty($sortingProperty));
+                                       } else {
+                                               return 0;
+                                       }
+                               }
+                       );
+
+                       switch (strtolower($sortingOrder)) {
+                               case 'descending':
+                               case 'desc':
+                                       $this->files = array_reverse($this->files);
+                                       break;
+                               case 'random':
+                               case 'rand':
+                                       shuffle($this->files);
+                                       break;
+                       }
+               }
+       }
+
+       /**
+        * Add a file object to the collection
+        *
+        * @param FileInterface $file The file object
+        * @return void
+        */
+       public function addFileObject(FileInterface $file) {
+               $this->files[] = $file;
+       }
+
+       /**
+        * Add multiple file objects to the collection
+        *
+        * @param FileInterface[] $files The file objects
+        * @return void
+        */
+       public function addFileObjects($files) {
+               $this->files = array_merge($this->files, $files);
+       }
+
+       /**
+        * Final getter method to fetch the accumulated data
+        *
+        * @return array
+        */
+       public function getFiles() {
+               return $this->files;
+       }
+
+       /**
+        * @return int
+        */
+       public function count() {
+               return count($this->files);
+       }
+
+       /**
+        * @return \TYPO3\CMS\Core\Log\Logger
+        */
+       protected function getLogger() {
+               return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
+       }
+
+       /**
+        * @return ResourceFactory
+        */
+       protected function getResourceFactory() {
+               if ($this->resourceFactory === NULL) {
+                       $this->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+               }
+               return $this->resourceFactory;
+       }
+
+       /**
+        * @return FileCollectionRepository
+        */
+       protected function getFileCollectionRepository() {
+               if ($this->fileCollectionRepository === NULL) {
+                       $this->fileCollectionRepository = GeneralUtility::makeInstance(FileCollectionRepository::class);
+               }
+               return $this->fileCollectionRepository;
+       }
+
+       /**
+        * @return FileRepository
+        */
+       protected function getFileRepository() {
+               if ($this->fileRepository === NULL) {
+                       $this->fileRepository = GeneralUtility::makeInstance(FileRepository::class);
+               }
+               return $this->fileRepository;
+       }
+}
index 983b299..3e67704 100644 (file)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Frontend\Tests\Unit\ContentObject;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
 use TYPO3\CMS\Frontend\ContentObject\FilesContentObject;
 use TYPO3\CMS\Frontend\ContentObject\TextContentObject;
+use TYPO3\CMS\Frontend\Resource\FileCollector;
 
 /**
  * Testcase for TYPO3\CMS\Frontend\ContentObject\FilesContentObject
@@ -54,7 +55,7 @@ class FilesContentObjectTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                        'FILES' => FilesContentObject::class,
                        'TEXT' => TextContentObject::class,
                ));
-               $this->subject = new FilesContentObject($contentObjectRenderer);
+               $this->subject = $this->getMock(FilesContentObject::class, array('getFileCollector'), array($contentObjectRenderer));
        }
 
        /**
@@ -215,14 +216,21 @@ class FilesContentObjectTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                ->with('name')
                                ->will($this->returnValue('File ' . $i));
 
-                       $fileReferenceMap[] = array($i, array(), FALSE, $fileReference);
+                       $fileReferenceMap[] = array($i, $fileReference);
                }
 
-               $resourceFactory = $this->getMock(\TYPO3\CMS\Core\Resource\ResourceFactory::class);
-               $resourceFactory->expects($this->any())
-                       ->method('getFileReferenceObject')
+               $fileRepository = $this->getMock(\TYPO3\CMS\Core\Resource\FileRepository::class);
+               $fileRepository->expects($this->any())
+                       ->method('findFileReferenceByUid')
                        ->will($this->returnValueMap($fileReferenceMap));
-               $this->subject->setFileFactory($resourceFactory);
+               $fileCollector = $this->getMock(FileCollector::class, array('getFileRepository'));
+               $fileCollector->expects($this->any())
+                       ->method('getFileRepository')
+                       ->will($this->returnValue($fileRepository));
+
+               $this->subject->expects($this->any())
+                       ->method('getFileCollector')
+                       ->will($this->returnValue($fileCollector));
 
                $this->assertSame($expected, $this->subject->render($configuration));
        }
@@ -392,7 +400,14 @@ class FilesContentObjectTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $resourceFactory->expects($this->any())
                        ->method('getFileObject')
                        ->will($this->returnValueMap($fileMap));
-               $this->subject->setFileFactory($resourceFactory);
+               $fileCollector = $this->getMock(FileCollector::class, array('getResourceFactory'));
+               $fileCollector->expects($this->any())
+                       ->method('getResourceFactory')
+                       ->will($this->returnValue($resourceFactory));
+
+               $this->subject->expects($this->any())
+                       ->method('getFileCollector')
+                       ->will($this->returnValue($fileCollector));
 
                $this->assertSame($expected, $this->subject->render($configuration));
        }
@@ -611,7 +626,13 @@ class FilesContentObjectTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $collectionRepository->expects($this->any())
                        ->method('findByUid')
                        ->will($this->returnValueMap($collectionMap));
-               $this->subject->setCollectionRepository($collectionRepository);
+               $fileCollector = $this->getMock(FileCollector::class, array('getFileCollectionRepository'));
+               $fileCollector->expects($this->any())
+                       ->method('getFileCollectionRepository')
+                       ->will($this->returnValue($collectionRepository));
+               $this->subject->expects($this->any())
+                       ->method('getFileCollector')
+                       ->will($this->returnValue($fileCollector));
 
                $this->assertSame($expected, $this->subject->render($configuration));
        }
@@ -826,11 +847,18 @@ class FilesContentObjectTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                        $folderMap[] = array($i . ':myfolder/', $folder);
                }
 
-               $fileFactory = $this->getMock(\TYPO3\CMS\Core\Resource\ResourceFactory::class);
-               $fileFactory->expects($this->any())
+               $resourceFactory = $this->getMock(\TYPO3\CMS\Core\Resource\ResourceFactory::class);
+               $resourceFactory->expects($this->any())
                        ->method('getFolderObjectFromCombinedIdentifier')
                        ->will($this->returnValueMap($folderMap));
-               $this->subject->setFileFactory($fileFactory);
+               $fileCollector = $this->getMock(FileCollector::class, array('getResourceFactory'));
+               $fileCollector->expects($this->any())
+                       ->method('getResourceFactory')
+                       ->will($this->returnValue($resourceFactory));
+
+               $this->subject->expects($this->any())
+                       ->method('getFileCollector')
+                       ->will($this->returnValue($fileCollector));
 
                $this->assertSame($expected, $this->subject->render($configuration));
        }