1<?php
2namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Core\Database\Query\QueryHelper;
18
19/**
20 * A factory for a data map to map a single table configured in $TCA on a domain object.
21 * @internal only to be used within Extbase, not part of TYPO3 Core API.
22 */
23class DataMapFactory implements \TYPO3\CMS\Core\SingletonInterface
24{
25    /**
26     * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
27     */
28    protected $reflectionService;
29
30    /**
31     * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
32     */
33    protected $configurationManager;
34
35    /**
36     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
37     */
38    protected $objectManager;
39
40    /**
41     * @var \TYPO3\CMS\Core\Cache\CacheManager
42     */
43    protected $cacheManager;
44
45    /**
46     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
47     */
48    protected $dataMapCache;
49
50    /**
51     * Runtime cache for data maps, to reduce number of calls to cache backend.
52     *
53     * @var array
54     */
55    protected $dataMaps = [];
56
57    /**
58     * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
59     */
60    public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService)
61    {
62        $this->reflectionService = $reflectionService;
63    }
64
65    /**
66     * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
67     */
68    public function injectConfigurationManager(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager)
69    {
70        $this->configurationManager = $configurationManager;
71    }
72
73    /**
74     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
75     */
76    public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
77    {
78        $this->objectManager = $objectManager;
79    }
80
81    /**
82     * @param \TYPO3\CMS\Core\Cache\CacheManager $cacheManager
83     */
84    public function injectCacheManager(\TYPO3\CMS\Core\Cache\CacheManager $cacheManager)
85    {
86        $this->cacheManager = $cacheManager;
87    }
88
89    /**
90     * Lifecycle method
91     */
92    public function initializeObject()
93    {
94        $this->dataMapCache = $this->cacheManager->getCache('extbase_datamapfactory_datamap');
95    }
96
97    /**
98     * Builds a data map by adding column maps for all the configured columns in the $TCA.
99     * It also resolves the type of values the column is holding and the typo of relation the column
100     * represents.
101     *
102     * @param string $className The class name you want to fetch the Data Map for
103     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap The data map
104     */
105    public function buildDataMap($className)
106    {
107        if (isset($this->dataMaps[$className])) {
108            return $this->dataMaps[$className];
109        }
110        $dataMap = $this->dataMapCache->get(str_replace('\\', '%', $className));
111        if ($dataMap === false) {
112            $dataMap = $this->buildDataMapInternal($className);
113            $this->dataMapCache->set(str_replace('\\', '%', $className), $dataMap);
114        }
115        $this->dataMaps[$className] = $dataMap;
116        return $dataMap;
117    }
118
119    /**
120     * Builds a data map by adding column maps for all the configured columns in the $TCA.
121     * It also resolves the type of values the column is holding and the typo of relation the column
122     * represents.
123     *
124     * @param string $className The class name you want to fetch the Data Map for
125     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidClassException
126     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap The data map
127     */
128    protected function buildDataMapInternal($className)
129    {
130        if (!class_exists($className)) {
131            throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidClassException(
132                'Could not find class definition for name "' . $className . '". This could be caused by a mis-spelling of the class name in the class definition.',
133                1476045117
134            );
135        }
136        $recordType = null;
137        $subclasses = [];
138        $tableName = $this->resolveTableName($className);
139        $columnMapping = [];
140        $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
141        $classSettings = $frameworkConfiguration['persistence']['classes'][$className] ?? null;
142        if ($classSettings !== null) {
143            if (isset($classSettings['subclasses']) && is_array($classSettings['subclasses'])) {
144                $subclasses = $this->resolveSubclassesRecursive($frameworkConfiguration['persistence']['classes'], $classSettings['subclasses']);
145            }
146            if (isset($classSettings['mapping']['recordType']) && $classSettings['mapping']['recordType'] !== '') {
147                $recordType = $classSettings['mapping']['recordType'];
148            }
149            if (isset($classSettings['mapping']['tableName']) && $classSettings['mapping']['tableName'] !== '') {
150                $tableName = $classSettings['mapping']['tableName'];
151            }
152            $classHierarchy = array_merge([$className], class_parents($className));
153            foreach ($classHierarchy as $currentClassName) {
154                if (in_array($currentClassName, [\TYPO3\CMS\Extbase\DomainObject\AbstractEntity::class, \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject::class])) {
155                    break;
156                }
157                $currentClassSettings = $frameworkConfiguration['persistence']['classes'][$currentClassName];
158                if ($currentClassSettings !== null) {
159                    if (isset($currentClassSettings['mapping']['columns']) && is_array($currentClassSettings['mapping']['columns'])) {
160                        \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($columnMapping, $currentClassSettings['mapping']['columns'], true, false);
161                    }
162                }
163            }
164        }
165        /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap $dataMap */
166        $dataMap = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap::class, $className, $tableName, $recordType, $subclasses);
167        $dataMap = $this->addMetaDataColumnNames($dataMap, $tableName);
168        // $classPropertyNames = $this->reflectionService->getClassPropertyNames($className);
169        $tcaColumnsDefinition = $this->getColumnsDefinition($tableName);
170        \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($tcaColumnsDefinition, $columnMapping);
171        // @todo Is this is too powerful?
172
173        foreach ($tcaColumnsDefinition as $columnName => $columnDefinition) {
174            if (isset($columnDefinition['mapOnProperty'])) {
175                $propertyName = $columnDefinition['mapOnProperty'];
176            } else {
177                $propertyName = \TYPO3\CMS\Core\Utility\GeneralUtility::underscoredToLowerCamelCase($columnName);
178            }
179            // if (in_array($propertyName, $classPropertyNames)) {
180            // @todo Enable check for property existence
181            $columnMap = $this->createColumnMap($columnName, $propertyName);
182            $propertyMetaData = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
183            $columnMap = $this->setType($columnMap, $columnDefinition['config']);
184            $columnMap = $this->setRelations($columnMap, $columnDefinition['config'], $propertyMetaData);
185            $columnMap = $this->setFieldEvaluations($columnMap, $columnDefinition['config']);
186            $dataMap->addColumnMap($columnMap);
187        }
188        return $dataMap;
189    }
190
191    /**
192     * Resolve the table name for the given class name
193     *
194     * @param string $className
195     * @return string The table name
196     */
197    protected function resolveTableName($className)
198    {
199        $className = ltrim($className, '\\');
200        $classNameParts = explode('\\', $className);
201        // Skip vendor and product name for core classes
202        if (strpos($className, 'TYPO3\\CMS\\') === 0) {
203            $classPartsToSkip = 2;
204        } else {
205            $classPartsToSkip = 1;
206        }
207        $tableName = 'tx_' . strtolower(implode('_', array_slice($classNameParts, $classPartsToSkip)));
208
209        return $tableName;
210    }
211
212    /**
213     * Resolves all subclasses for the given set of (sub-)classes.
214     * The whole classes configuration is used to determine all subclasses recursively.
215     *
216     * @param array $classesConfiguration The framework configuration part [persistence][classes].
217     * @param array $subclasses An array of subclasses defined via TypoScript
218     * @return array An numeric array that contains all available subclasses-strings as values.
219     */
220    protected function resolveSubclassesRecursive(array $classesConfiguration, array $subclasses)
221    {
222        $allSubclasses = [];
223        foreach ($subclasses as $subclass) {
224            $allSubclasses[] = $subclass;
225            if (isset($classesConfiguration[$subclass]['subclasses']) && is_array($classesConfiguration[$subclass]['subclasses'])) {
226                $childSubclasses = $this->resolveSubclassesRecursive($classesConfiguration, $classesConfiguration[$subclass]['subclasses']);
227                $allSubclasses = array_merge($allSubclasses, $childSubclasses);
228            }
229        }
230        return $allSubclasses;
231    }
232
233    /**
234     * Returns the TCA ctrl section of the specified table; or NULL if not set
235     *
236     * @param string $tableName An optional table name to fetch the columns definition from
237     * @return array The TCA columns definition
238     */
239    protected function getControlSection($tableName)
240    {
241        return (isset($GLOBALS['TCA'][$tableName]['ctrl']) && is_array($GLOBALS['TCA'][$tableName]['ctrl']))
242            ? $GLOBALS['TCA'][$tableName]['ctrl']
243            : null;
244    }
245
246    /**
247     * Returns the TCA columns array of the specified table
248     *
249     * @param string $tableName An optional table name to fetch the columns definition from
250     * @return array The TCA columns definition
251     */
252    protected function getColumnsDefinition($tableName)
253    {
254        return is_array($GLOBALS['TCA'][$tableName]['columns']) ? $GLOBALS['TCA'][$tableName]['columns'] : [];
255    }
256
257    /**
258     * @param DataMap $dataMap
259     * @param string $tableName
260     * @return DataMap
261     */
262    protected function addMetaDataColumnNames(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap $dataMap, $tableName)
263    {
264        $controlSection = $GLOBALS['TCA'][$tableName]['ctrl'];
265        $dataMap->setPageIdColumnName('pid');
266        if (isset($controlSection['tstamp'])) {
267            $dataMap->setModificationDateColumnName($controlSection['tstamp']);
268        }
269        if (isset($controlSection['crdate'])) {
270            $dataMap->setCreationDateColumnName($controlSection['crdate']);
271        }
272        if (isset($controlSection['cruser_id'])) {
273            $dataMap->setCreatorColumnName($controlSection['cruser_id']);
274        }
275        if (isset($controlSection['delete'])) {
276            $dataMap->setDeletedFlagColumnName($controlSection['delete']);
277        }
278        if (isset($controlSection['languageField'])) {
279            $dataMap->setLanguageIdColumnName($controlSection['languageField']);
280        }
281        if (isset($controlSection['transOrigPointerField'])) {
282            $dataMap->setTranslationOriginColumnName($controlSection['transOrigPointerField']);
283        }
284        if (isset($controlSection['transOrigDiffSourceField'])) {
285            $dataMap->setTranslationOriginDiffSourceName($controlSection['transOrigDiffSourceField']);
286        }
287        if (isset($controlSection['type'])) {
288            $dataMap->setRecordTypeColumnName($controlSection['type']);
289        }
290        if (isset($controlSection['rootLevel'])) {
291            $dataMap->setRootLevel($controlSection['rootLevel']);
292        }
293        if (isset($controlSection['is_static'])) {
294            $dataMap->setIsStatic($controlSection['is_static']);
295        }
296        if (isset($controlSection['enablecolumns']['disabled'])) {
297            $dataMap->setDisabledFlagColumnName($controlSection['enablecolumns']['disabled']);
298        }
299        if (isset($controlSection['enablecolumns']['starttime'])) {
300            $dataMap->setStartTimeColumnName($controlSection['enablecolumns']['starttime']);
301        }
302        if (isset($controlSection['enablecolumns']['endtime'])) {
303            $dataMap->setEndTimeColumnName($controlSection['enablecolumns']['endtime']);
304        }
305        if (isset($controlSection['enablecolumns']['fe_group'])) {
306            $dataMap->setFrontEndUserGroupColumnName($controlSection['enablecolumns']['fe_group']);
307        }
308        return $dataMap;
309    }
310
311    /**
312     * Set the table column type
313     *
314     * @param ColumnMap $columnMap
315     * @param array $columnConfiguration
316     * @return ColumnMap
317     */
318    protected function setType(ColumnMap $columnMap, $columnConfiguration)
319    {
320        $tableColumnType = $columnConfiguration['type'] ?? null;
321        $columnMap->setType(\TYPO3\CMS\Core\DataHandling\TableColumnType::cast($tableColumnType));
322        $tableColumnSubType = $columnConfiguration['internal_type'] ?? null;
323        $columnMap->setInternalType(\TYPO3\CMS\Core\DataHandling\TableColumnSubType::cast($tableColumnSubType));
324
325        return $columnMap;
326    }
327
328    /**
329     * This method tries to determine the type of type of relation to other tables and sets it based on
330     * the $TCA column configuration
331     *
332     * @param ColumnMap $columnMap The column map
333     * @param array|null $columnConfiguration The column configuration from $TCA
334     * @param array $propertyMetaData The property metadata as delivered by the reflection service
335     * @return ColumnMap
336     */
337    protected function setRelations(ColumnMap $columnMap, $columnConfiguration, $propertyMetaData)
338    {
339        if (isset($columnConfiguration)) {
340            if (isset($columnConfiguration['MM'])) {
341                $columnMap = $this->setManyToManyRelation($columnMap, $columnConfiguration);
342            } elseif (isset($propertyMetaData['elementType'])) {
343                $columnMap = $this->setOneToManyRelation($columnMap, $columnConfiguration);
344            } elseif (isset($propertyMetaData['type']) && strpbrk($propertyMetaData['type'], '_\\') !== false) {
345                $columnMap = $this->setOneToOneRelation($columnMap, $columnConfiguration);
346            } elseif (
347                isset($columnConfiguration['type'], $columnConfiguration['renderType'])
348                && $columnConfiguration['type'] === 'select'
349                && (
350                    $columnConfiguration['renderType'] !== 'selectSingle'
351                    || (isset($columnConfiguration['maxitems']) && $columnConfiguration['maxitems'] > 1)
352                )
353            ) {
354                $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_MANY);
355            } elseif (
356                isset($columnConfiguration['type']) && $columnConfiguration['type'] === 'group'
357                && (!isset($columnConfiguration['maxitems']) || $columnConfiguration['maxitems'] > 1)
358            ) {
359                $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_MANY);
360            } else {
361                $columnMap->setTypeOfRelation(ColumnMap::RELATION_NONE);
362            }
363        } else {
364            $columnMap->setTypeOfRelation(ColumnMap::RELATION_NONE);
365        }
366        return $columnMap;
367    }
368
369    /**
370     * Sets field evaluations based on $TCA column configuration.
371     *
372     * @param ColumnMap $columnMap The column map
373     * @param array|null $columnConfiguration The column configuration from $TCA
374     * @return ColumnMap
375     */
376    protected function setFieldEvaluations(ColumnMap $columnMap, array $columnConfiguration = null)
377    {
378        if (!empty($columnConfiguration['eval'])) {
379            $fieldEvaluations = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $columnConfiguration['eval'], true);
380            $dateTimeTypes = QueryHelper::getDateTimeTypes();
381
382            if (!empty(array_intersect($dateTimeTypes, $fieldEvaluations)) && !empty($columnConfiguration['dbType'])) {
383                $columnMap->setDateTimeStorageFormat($columnConfiguration['dbType']);
384            }
385        }
386
387        return $columnMap;
388    }
389
390    /**
391     * This method sets the configuration for a 1:1 relation based on
392     * the $TCA column configuration
393     *
394     * @param ColumnMap $columnMap The column map
395     * @param array|null $columnConfiguration The column configuration from $TCA
396     * @return ColumnMap
397     */
398    protected function setOneToOneRelation(ColumnMap $columnMap, array $columnConfiguration = null)
399    {
400        $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_ONE);
401        $columnMap->setChildTableName($columnConfiguration['foreign_table']);
402        $columnMap->setChildTableWhereStatement($columnConfiguration['foreign_table_where'] ?? null);
403        $columnMap->setChildSortByFieldName($columnConfiguration['foreign_sortby'] ?? null);
404        $columnMap->setParentKeyFieldName($columnConfiguration['foreign_field'] ?? null);
405        $columnMap->setParentTableFieldName($columnConfiguration['foreign_table_field'] ?? null);
406        if (is_array($columnConfiguration['foreign_match_fields'])) {
407            $columnMap->setRelationTableMatchFields($columnConfiguration['foreign_match_fields']);
408        }
409        return $columnMap;
410    }
411
412    /**
413     * This method sets the configuration for a 1:n relation based on
414     * the $TCA column configuration
415     *
416     * @param ColumnMap $columnMap The column map
417     * @param array|null $columnConfiguration The column configuration from $TCA
418     * @return ColumnMap
419     */
420    protected function setOneToManyRelation(ColumnMap $columnMap, array $columnConfiguration = null)
421    {
422        $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_MANY);
423        $columnMap->setChildTableName($columnConfiguration['foreign_table']);
424        $columnMap->setChildTableWhereStatement($columnConfiguration['foreign_table_where'] ?? null);
425        $columnMap->setChildSortByFieldName($columnConfiguration['foreign_sortby'] ?? null);
426        $columnMap->setParentKeyFieldName($columnConfiguration['foreign_field'] ?? null);
427        $columnMap->setParentTableFieldName($columnConfiguration['foreign_table_field'] ?? null);
428        if (is_array($columnConfiguration['foreign_match_fields'] ?? null)) {
429            $columnMap->setRelationTableMatchFields($columnConfiguration['foreign_match_fields']);
430        }
431        return $columnMap;
432    }
433
434    /**
435     * This method sets the configuration for a m:n relation based on
436     * the $TCA column configuration
437     *
438     * @param ColumnMap $columnMap The column map
439     * @param array|null $columnConfiguration The column configuration from $TCA
440     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedRelationException
441     * @return ColumnMap
442     */
443    protected function setManyToManyRelation(ColumnMap $columnMap, array $columnConfiguration = null)
444    {
445        if (isset($columnConfiguration['MM'])) {
446            $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY);
447            $columnMap->setChildTableName($columnConfiguration['foreign_table']);
448            $columnMap->setChildTableWhereStatement($columnConfiguration['foreign_table_where'] ?? null);
449            $columnMap->setRelationTableName($columnConfiguration['MM']);
450            if (isset($columnConfiguration['MM_match_fields']) && is_array($columnConfiguration['MM_match_fields'])) {
451                $columnMap->setRelationTableMatchFields($columnConfiguration['MM_match_fields']);
452            }
453            if (isset($columnConfiguration['MM_insert_fields']) && is_array($columnConfiguration['MM_insert_fields'])) {
454                $columnMap->setRelationTableInsertFields($columnConfiguration['MM_insert_fields']);
455            }
456            $columnMap->setRelationTableWhereStatement($columnConfiguration['MM_table_where'] ?? null);
457            if (!empty($columnConfiguration['MM_opposite_field'])) {
458                $columnMap->setParentKeyFieldName('uid_foreign');
459                $columnMap->setChildKeyFieldName('uid_local');
460                $columnMap->setChildSortByFieldName('sorting_foreign');
461            } else {
462                $columnMap->setParentKeyFieldName('uid_local');
463                $columnMap->setChildKeyFieldName('uid_foreign');
464                $columnMap->setChildSortByFieldName('sorting');
465            }
466        } else {
467            throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedRelationException('The given information to build a many-to-many-relation was not sufficient. Check your TCA definitions. mm-relations with IRRE must have at least a defined "MM" or "foreign_selector".', 1268817963);
468        }
469        if ($this->getControlSection($columnMap->getRelationTableName()) !== null) {
470            $columnMap->setRelationTablePageIdColumnName('pid');
471        }
472        return $columnMap;
473    }
474
475    /**
476     * Creates the ColumnMap object for the given columnName and propertyName
477     *
478     * @param string $columnName
479     * @param string $propertyName
480     *
481     * @return ColumnMap
482     */
483    protected function createColumnMap($columnName, $propertyName)
484    {
485        return $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::class, $columnName, $propertyName);
486    }
487}
488