Passed
Push — master ( e47dd8...3ef27a )
by
unknown
13:30
created

DataMapper::isPersistableProperty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
16
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
17
18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use TYPO3\CMS\Core\Database\Query\QueryHelper;
20
use TYPO3\CMS\Core\Utility\GeneralUtility;
21
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
22
use TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent;
23
use TYPO3\CMS\Extbase\Object\Exception\CannotReconstituteObjectException;
24
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
25
use TYPO3\CMS\Extbase\Persistence;
26
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, TYPO3\CMS\Extbase\Persis...eneric\Mapper\Exception. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
27
use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException;
28
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
29
use TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage;
30
use TYPO3\CMS\Extbase\Persistence\Generic\LoadingStrategyInterface;
31
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception\NonExistentPropertyException;
32
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception\UnknownPropertyTypeException;
33
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory;
34
use TYPO3\CMS\Extbase\Persistence\Generic\Query;
35
use TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface;
36
use TYPO3\CMS\Extbase\Persistence\Generic\Session;
37
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
38
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
39
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
40
use TYPO3\CMS\Extbase\Reflection\ClassSchema;
41
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException;
42
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
43
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
44
45
/**
46
 * A mapper to map database tables configured in $TCA on domain objects.
47
 * @internal only to be used within Extbase, not part of TYPO3 Core API.
48
 */
49
class DataMapper
50
{
51
    /**
52
     * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
53
     */
54
    protected $reflectionService;
55
56
    /**
57
     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory
58
     */
59
    protected $qomFactory;
60
61
    /**
62
     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Session
63
     */
64
    protected $persistenceSession;
65
66
    /**
67
     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory
68
     */
69
    protected $dataMapFactory;
70
71
    /**
72
     * @var \TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface
73
     */
74
    protected $queryFactory;
75
76
    /**
77
     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
78
     */
79
    protected $objectManager;
80
81
    /**
82
     * @var EventDispatcherInterface
83
     */
84
    protected $eventDispatcher;
85
86
    /**
87
     * @var QueryInterface|null
88
     */
89
    protected $query;
90
91
    /**
92
     * DataMapper constructor.
93
     * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
94
     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory $qomFactory
95
     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Session $persistenceSession
96
     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory $dataMapFactory
97
     * @param \TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface $queryFactory
98
     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
99
     * @param EventDispatcherInterface $eventDispatcher
100
     */
101
    public function __construct(
102
        ReflectionService $reflectionService,
103
        QueryObjectModelFactory $qomFactory,
104
        Session $persistenceSession,
105
        DataMapFactory $dataMapFactory,
106
        QueryFactoryInterface $queryFactory,
107
        ObjectManagerInterface $objectManager,
108
        EventDispatcherInterface $eventDispatcher
109
    ) {
110
        $this->reflectionService = $reflectionService;
111
        $this->qomFactory = $qomFactory;
112
        $this->persistenceSession = $persistenceSession;
113
        $this->dataMapFactory = $dataMapFactory;
114
        $this->queryFactory = $queryFactory;
115
        $this->objectManager = $objectManager;
116
        $this->eventDispatcher = $eventDispatcher;
117
    }
118
119
    /**
120
     * @param QueryInterface $query
121
     */
122
    public function setQuery(QueryInterface $query): void
123
    {
124
        $this->query = $query;
125
    }
126
127
    /**
128
     * Maps the given rows on objects
129
     *
130
     * @param string $className The name of the class
131
     * @param array $rows An array of arrays with field_name => value pairs
132
     * @return array An array of objects of the given class
133
     */
134
    public function map($className, array $rows)
135
    {
136
        $objects = [];
137
        foreach ($rows as $row) {
138
            $objects[] = $this->mapSingleRow($this->getTargetType($className, $row), $row);
139
        }
140
        return $objects;
141
    }
142
143
    /**
144
     * Returns the target type for the given row.
145
     *
146
     * @param string $className The name of the class
147
     * @param array $row A single array with field_name => value pairs
148
     * @return string The target type (a class name)
149
     */
150
    public function getTargetType($className, array $row)
151
    {
152
        $dataMap = $this->getDataMap($className);
153
        $targetType = $className;
154
        if ($dataMap->getRecordTypeColumnName() !== null) {
0 ignored issues
show
introduced by
The condition $dataMap->getRecordTypeColumnName() !== null is always true.
Loading history...
155
            foreach ($dataMap->getSubclasses() as $subclassName) {
156
                $recordSubtype = $this->getDataMap($subclassName)->getRecordType();
157
                if ((string)$row[$dataMap->getRecordTypeColumnName()] === (string)$recordSubtype) {
158
                    $targetType = $subclassName;
159
                    break;
160
                }
161
            }
162
        }
163
        return $targetType;
164
    }
165
166
    /**
167
     * Maps a single row on an object of the given class
168
     *
169
     * @param string $className The name of the target class
170
     * @param array $row A single array with field_name => value pairs
171
     * @return object An object of the given class
172
     */
173
    protected function mapSingleRow($className, array $row)
174
    {
175
        if ($this->persistenceSession->hasIdentifier($row['uid'], $className)) {
176
            $object = $this->persistenceSession->getObjectByIdentifier($row['uid'], $className);
177
        } else {
178
            $object = $this->createEmptyObject($className);
179
            $this->persistenceSession->registerObject($object, $row['uid']);
180
            $this->thawProperties($object, $row);
181
            $event = new AfterObjectThawedEvent($object, $row);
182
            $this->eventDispatcher->dispatch($event);
183
            $object->_memorizeCleanState();
0 ignored issues
show
Bug introduced by
The method _memorizeCleanState() does not exist on TYPO3\CMS\Extbase\Domain...t\DomainObjectInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to TYPO3\CMS\Extbase\Domain...t\DomainObjectInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

183
            $object->/** @scrutinizer ignore-call */ 
184
                     _memorizeCleanState();
Loading history...
184
            $this->persistenceSession->registerReconstitutedEntity($object);
185
        }
186
        return $object;
187
    }
188
189
    /**
190
     * Creates a skeleton of the specified object
191
     *
192
     * @param string $className Name of the class to create a skeleton for
193
     * @throws CannotReconstituteObjectException
194
     * @return DomainObjectInterface The object skeleton
195
     */
196
    protected function createEmptyObject($className)
197
    {
198
        // Note: The class_implements() function also invokes autoload to assure that the interfaces
199
        // and the class are loaded. Would end up with __PHP_Incomplete_Class without it.
200
        if (!in_array(DomainObjectInterface::class, class_implements($className))) {
201
            throw new CannotReconstituteObjectException('Cannot create empty instance of the class "' . $className
202
                . '" because it does not implement the TYPO3\\CMS\\Extbase\\DomainObject\\DomainObjectInterface.', 1234386924);
203
        }
204
        $object = $this->objectManager->getEmptyObject($className);
205
        return $object;
206
    }
207
208
    /**
209
     * Sets the given properties on the object.
210
     *
211
     * @param DomainObjectInterface $object The object to set properties on
212
     * @param array $row
213
     * @throws NonExistentPropertyException
214
     * @throws UnknownPropertyTypeException
215
     */
216
    protected function thawProperties(DomainObjectInterface $object, array $row)
217
    {
218
        $className = get_class($object);
219
        $classSchema = $this->reflectionService->getClassSchema($className);
220
        $dataMap = $this->getDataMap($className);
221
        $object->_setProperty('uid', (int)$row['uid']);
222
        $object->_setProperty('pid', (int)($row['pid'] ?? 0));
223
        $object->_setProperty('_localizedUid', (int)$row['uid']);
224
        $object->_setProperty('_versionedUid', (int)$row['uid']);
225
        if ($dataMap->getLanguageIdColumnName() !== null) {
0 ignored issues
show
introduced by
The condition $dataMap->getLanguageIdColumnName() !== null is always true.
Loading history...
226
            $object->_setProperty('_languageUid', (int)$row[$dataMap->getLanguageIdColumnName()]);
227
            if (isset($row['_LOCALIZED_UID'])) {
228
                $object->_setProperty('_localizedUid', (int)$row['_LOCALIZED_UID']);
229
            }
230
        }
231
        if (!empty($row['_ORIG_uid']) && !empty($GLOBALS['TCA'][$dataMap->getTableName()]['ctrl']['versioningWS'])) {
232
            $object->_setProperty('_versionedUid', (int)$row['_ORIG_uid']);
233
        }
234
        $properties = $object->_getProperties();
235
        foreach ($properties as $propertyName => $propertyValue) {
236
            if (!$dataMap->isPersistableProperty($propertyName)) {
237
                continue;
238
            }
239
            $columnMap = $dataMap->getColumnMap($propertyName);
240
            $columnName = $columnMap->getColumnName();
241
242
            try {
243
                $property = $classSchema->getProperty($propertyName);
244
            } catch (NoSuchPropertyException $e) {
245
                throw new NonExistentPropertyException(
246
                    'The type of property ' . $className . '::' . $propertyName . ' could not be identified, ' .
247
                    'as property ' . $propertyName . ' is unknown to the ' . ClassSchema::class . ' instance of class ' .
248
                    $className . '. Please make sure said property exists and that you cleared all caches to trigger ' .
249
                    'a new build of said ' . ClassSchema::class . ' instance.',
250
                    1580056272
251
                );
252
            }
253
254
            $propertyType = $property->getType();
255
            if ($propertyType === null) {
256
                throw new UnknownPropertyTypeException(
257
                    'The type of property ' . $className . '::' . $propertyName . ' could not be identified, therefore the desired value (' .
258
                    var_export($propertyValue, true) . ') cannot be mapped onto it. The type of a class property is usually defined via php doc blocks. ' .
259
                    'Make sure the property has a valid @var tag set which defines the type.',
260
                    1579965021
261
                );
262
            }
263
            $propertyValue = null;
264
            if (isset($row[$columnName])) {
265
                switch ($propertyType) {
266
                    case 'int':
267
                    case 'integer':
268
                        $propertyValue = (int)$row[$columnName];
269
                        break;
270
                    case 'float':
271
                        $propertyValue = (double)$row[$columnName];
272
                        break;
273
                    case 'bool':
274
                    case 'boolean':
275
                        $propertyValue = (bool)$row[$columnName];
276
                        break;
277
                    case 'string':
278
                        $propertyValue = (string)$row[$columnName];
279
                        break;
280
                    case 'array':
281
                        // $propertyValue = $this->mapArray($row[$columnName]); // Not supported, yet!
282
                        break;
283
                    case \SplObjectStorage::class:
284
                    case ObjectStorage::class:
285
                        $propertyValue = $this->mapResultToPropertyValue(
286
                            $object,
287
                            $propertyName,
288
                            $this->fetchRelated($object, $propertyName, $row[$columnName])
289
                        );
290
                        break;
291
                    default:
292
                        if (is_subclass_of($propertyType, \DateTimeInterface::class)) {
293
                            $propertyValue = $this->mapDateTime(
294
                                $row[$columnName],
295
                                $columnMap->getDateTimeStorageFormat(),
296
                                $propertyType
297
                            );
298
                        } elseif (TypeHandlingUtility::isCoreType($propertyType)) {
299
                            $propertyValue = $this->mapCoreType($propertyType, $row[$columnName]);
300
                        } else {
301
                            $propertyValue = $this->mapObjectToClassProperty(
302
                                $object,
303
                                $propertyName,
304
                                $row[$columnName]
305
                            );
306
                        }
307
308
                }
309
            }
310
            if ($propertyValue !== null) {
311
                $object->_setProperty($propertyName, $propertyValue);
312
            }
313
        }
314
    }
315
316
    /**
317
     * Map value to a core type
318
     *
319
     * @param string $type
320
     * @param mixed $value
321
     * @return \TYPO3\CMS\Core\Type\TypeInterface
322
     */
323
    protected function mapCoreType($type, $value)
324
    {
325
        return new $type($value);
326
    }
327
328
    /**
329
     * Creates a DateTime from a unix timestamp or date/datetime/time value.
330
     * If the input is empty, NULL is returned.
331
     *
332
     * @param int|string $value Unix timestamp or date/datetime/time value
333
     * @param string|null $storageFormat Storage format for native date/datetime/time fields
334
     * @param string $targetType The object class name to be created
335
     * @return \DateTimeInterface
336
     */
337
    protected function mapDateTime($value, $storageFormat = null, $targetType = \DateTime::class)
338
    {
339
        $dateTimeTypes = QueryHelper::getDateTimeTypes();
340
341
        // Invalid values are converted to NULL
342
        if (empty($value) || $value === '0000-00-00' || $value === '0000-00-00 00:00:00' || $value === '00:00:00') {
343
            return null;
344
        }
345
        if (!in_array($storageFormat, $dateTimeTypes, true)) {
346
            // Integer timestamps are also stored "as is" in the database, but are UTC by definition,
347
            // so we convert the timestamp to a ISO representation.
348
            $value = date('c', (int)$value);
349
        }
350
        // All date/datetime/time values are stored in the database "as is", independent of any time zone information.
351
        // It is therefore only important to use the same time zone in PHP when storing and retrieving the values.
352
        return GeneralUtility::makeInstance($targetType, $value);
353
    }
354
355
    /**
356
     * Fetches a collection of objects related to a property of a parent object
357
     *
358
     * @param DomainObjectInterface $parentObject The object instance this proxy is part of
359
     * @param string $propertyName The name of the proxied property in it's parent
360
     * @param mixed $fieldValue The raw field value.
361
     * @param bool $enableLazyLoading A flag indication if the related objects should be lazy loaded
362
     * @return \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage|Persistence\QueryResultInterface The result
363
     */
364
    public function fetchRelated(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $enableLazyLoading = true)
365
    {
366
        $property = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
367
        if ($enableLazyLoading === true && $property->isLazy()) {
368
            if ($property->getType() === ObjectStorage::class) {
369
                $result = $this->objectManager->get(LazyObjectStorage::class, $parentObject, $propertyName, $fieldValue, $this);
370
            } else {
371
                if (empty($fieldValue)) {
372
                    $result = null;
373
                } else {
374
                    $result = $this->objectManager->get(LazyLoadingProxy::class, $parentObject, $propertyName, $fieldValue, $this);
375
                }
376
            }
377
        } else {
378
            $result = $this->fetchRelatedEager($parentObject, $propertyName, $fieldValue);
379
        }
380
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type array which is incompatible with the documented return type TYPO3\CMS\Extbase\Persis...ce\QueryResultInterface.
Loading history...
381
    }
382
383
    /**
384
     * Fetches the related objects from the storage backend.
385
     *
386
     * @param DomainObjectInterface $parentObject The object instance this proxy is part of
387
     * @param string $propertyName The name of the proxied property in it's parent
388
     * @param mixed $fieldValue The raw field value.
389
     * @return mixed
390
     */
391
    protected function fetchRelatedEager(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '')
392
    {
393
        return $fieldValue === '' ? $this->getEmptyRelationValue($parentObject, $propertyName) : $this->getNonEmptyRelationValue($parentObject, $propertyName, $fieldValue);
394
    }
395
396
    /**
397
     * @param DomainObjectInterface $parentObject
398
     * @param string $propertyName
399
     * @return array|null
400
     */
401
    protected function getEmptyRelationValue(DomainObjectInterface $parentObject, $propertyName)
402
    {
403
        $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
404
        $relatesToOne = $columnMap->getTypeOfRelation() == ColumnMap::RELATION_HAS_ONE;
405
        return $relatesToOne ? null : [];
406
    }
407
408
    /**
409
     * @param DomainObjectInterface $parentObject
410
     * @param string $propertyName
411
     * @param string $fieldValue
412
     * @return Persistence\QueryResultInterface
413
     */
414
    protected function getNonEmptyRelationValue(DomainObjectInterface $parentObject, $propertyName, $fieldValue)
415
    {
416
        $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
417
        return $query->execute();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->execute() also could return the type array which is incompatible with the documented return type TYPO3\CMS\Extbase\Persistence\QueryResultInterface.
Loading history...
418
    }
419
420
    /**
421
     * Builds and returns the prepared query, ready to be executed.
422
     *
423
     * @param DomainObjectInterface $parentObject
424
     * @param string $propertyName
425
     * @param string $fieldValue
426
     * @return Persistence\QueryInterface
427
     */
428
    protected function getPreparedQuery(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '')
429
    {
430
        $dataMap = $this->getDataMap(get_class($parentObject));
431
        $columnMap = $dataMap->getColumnMap($propertyName);
432
        $type = $this->getType(get_class($parentObject), $propertyName);
433
        $query = $this->queryFactory->create($type);
434
        if ($this->query && $query instanceof Query) {
435
            $query->setParentQuery($this->query);
436
        }
437
        $query->getQuerySettings()->setRespectStoragePage(false);
438
        $query->getQuerySettings()->setRespectSysLanguage(false);
439
440
        // we always want to overlay relations as most of the time they are stored in db using default lang uids
441
        $query->getQuerySettings()->setLanguageOverlayMode(true);
442
        if ($this->query) {
443
            $query->getQuerySettings()->setLanguageUid($this->query->getQuerySettings()->getLanguageUid());
444
445
            if ($dataMap->getLanguageIdColumnName() !== null && !$this->query->getQuerySettings()->getRespectSysLanguage()) {
446
                //pass language of parent record to child objects, so they can be overlaid correctly in case
447
                //e.g. findByUid is used.
448
                //the languageUid is used for getRecordOverlay later on, despite RespectSysLanguage being false
449
                $languageUid = (int)$parentObject->_getProperty('_languageUid');
450
                $query->getQuerySettings()->setLanguageUid($languageUid);
451
            }
452
        }
453
454
        if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
455
            if ($columnMap->getChildSortByFieldName() !== null) {
456
                $query->setOrderings([$columnMap->getChildSortByFieldName() => QueryInterface::ORDER_ASCENDING]);
457
            }
458
        } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
459
            $query->setSource($this->getSource($parentObject, $propertyName));
460
            if ($columnMap->getChildSortByFieldName() !== null) {
461
                $query->setOrderings([$columnMap->getChildSortByFieldName() => QueryInterface::ORDER_ASCENDING]);
462
            }
463
        }
464
        $query->matching($this->getConstraint($query, $parentObject, $propertyName, $fieldValue, (array)$columnMap->getRelationTableMatchFields()));
465
        return $query;
466
    }
467
468
    /**
469
     * Builds and returns the constraint for multi value properties.
470
     *
471
     * @param Persistence\QueryInterface $query
472
     * @param DomainObjectInterface $parentObject
473
     * @param string $propertyName
474
     * @param string $fieldValue
475
     * @param array $relationTableMatchFields
476
     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint
477
     */
478
    protected function getConstraint(QueryInterface $query, DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $relationTableMatchFields = [])
479
    {
480
        $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
481
        if ($columnMap->getParentKeyFieldName() !== null) {
482
            $constraint = $query->equals($columnMap->getParentKeyFieldName(), $parentObject);
483
            if ($columnMap->getParentTableFieldName() !== null) {
484
                $constraint = $query->logicalAnd(
485
                    $constraint,
486
                    $query->equals($columnMap->getParentTableFieldName(), $this->getDataMap(get_class($parentObject))->getTableName())
0 ignored issues
show
Unused Code introduced by
The call to TYPO3\CMS\Extbase\Persis...Interface::logicalAnd() has too many arguments starting with $query->equals($columnMa...ject))->getTableName()). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

486
                /** @scrutinizer ignore-call */ 
487
                $constraint = $query->logicalAnd(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
487
                );
488
            }
489
        } else {
490
            $constraint = $query->in('uid', GeneralUtility::intExplode(',', $fieldValue));
491
        }
492
        if (!empty($relationTableMatchFields)) {
493
            foreach ($relationTableMatchFields as $relationTableMatchFieldName => $relationTableMatchFieldValue) {
494
                $constraint = $query->logicalAnd($constraint, $query->equals($relationTableMatchFieldName, $relationTableMatchFieldValue));
495
            }
496
        }
497
        return $constraint;
498
    }
499
500
    /**
501
     * Builds and returns the source to build a join for a m:n relation.
502
     *
503
     * @param DomainObjectInterface $parentObject
504
     * @param string $propertyName
505
     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source
506
     */
507
    protected function getSource(DomainObjectInterface $parentObject, $propertyName)
508
    {
509
        $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
510
        $left = $this->qomFactory->selector(null, $columnMap->getRelationTableName());
511
        $childClassName = $this->getType(get_class($parentObject), $propertyName);
512
        $right = $this->qomFactory->selector($childClassName, $columnMap->getChildTableName());
513
        $joinCondition = $this->qomFactory->equiJoinCondition($columnMap->getRelationTableName(), $columnMap->getChildKeyFieldName(), $columnMap->getChildTableName(), 'uid');
514
        $source = $this->qomFactory->join($left, $right, Query::JCR_JOIN_TYPE_INNER, $joinCondition);
515
        return $source;
516
    }
517
518
    /**
519
     * Returns the mapped classProperty from the identityMap or
520
     * mapResultToPropertyValue()
521
     *
522
     * If the field value is empty and the column map has no parent key field name,
523
     * the relation will be empty. If the persistence session has a registered object of
524
     * the correct type and identity (fieldValue), this function returns that object.
525
     * Otherwise, it proceeds with mapResultToPropertyValue().
526
     *
527
     * @param DomainObjectInterface $parentObject
528
     * @param string $propertyName
529
     * @param mixed $fieldValue the raw field value
530
     * @return mixed
531
     * @see mapResultToPropertyValue()
532
     */
533
    protected function mapObjectToClassProperty(DomainObjectInterface $parentObject, $propertyName, $fieldValue)
534
    {
535
        if ($this->propertyMapsByForeignKey($parentObject, $propertyName)) {
536
            $result = $this->fetchRelated($parentObject, $propertyName, $fieldValue);
537
            $propertyValue = $this->mapResultToPropertyValue($parentObject, $propertyName, $result);
538
        } else {
539
            if ($fieldValue === '') {
540
                $propertyValue = $this->getEmptyRelationValue($parentObject, $propertyName);
541
            } else {
542
                $property = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
543
                if ($this->persistenceSession->hasIdentifier($fieldValue, $property->getType())) {
544
                    $propertyValue = $this->persistenceSession->getObjectByIdentifier($fieldValue, $property->getType());
545
                } else {
546
                    $result = $this->fetchRelated($parentObject, $propertyName, $fieldValue);
547
                    $propertyValue = $this->mapResultToPropertyValue($parentObject, $propertyName, $result);
548
                }
549
            }
550
        }
551
552
        return $propertyValue;
553
    }
554
555
    /**
556
     * Checks if the relation is based on a foreign key.
557
     *
558
     * @param DomainObjectInterface $parentObject
559
     * @param string $propertyName
560
     * @return bool TRUE if the property is mapped
561
     */
562
    protected function propertyMapsByForeignKey(DomainObjectInterface $parentObject, $propertyName)
563
    {
564
        $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
565
        return $columnMap->getParentKeyFieldName() !== null;
566
    }
567
568
    /**
569
     * Returns the given result as property value of the specified property type.
570
     *
571
     * @param DomainObjectInterface $parentObject
572
     * @param string $propertyName
573
     * @param mixed $result The result
574
     * @return mixed
575
     */
576
    public function mapResultToPropertyValue(DomainObjectInterface $parentObject, $propertyName, $result)
577
    {
578
        $propertyValue = null;
579
        if ($result instanceof LoadingStrategyInterface) {
580
            $propertyValue = $result;
581
        } else {
582
            $property = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
583
            if (in_array($property->getType(), ['array', \ArrayObject::class, \SplObjectStorage::class, ObjectStorage::class], true)) {
584
                $objects = [];
585
                foreach ($result as $value) {
586
                    $objects[] = $value;
587
                }
588
                if ($property->getType() === \ArrayObject::class) {
589
                    $propertyValue = new \ArrayObject($objects);
590
                } elseif ($property->getType() === ObjectStorage::class) {
591
                    $propertyValue = new ObjectStorage();
592
                    foreach ($objects as $object) {
593
                        $propertyValue->attach($object);
594
                    }
595
                    $propertyValue->_memorizeCleanState();
596
                } else {
597
                    $propertyValue = $objects;
598
                }
599
            } elseif (strpbrk((string)$property->getType(), '_\\') !== false) {
600
                // @todo: check the strpbrk function call. Seems to be a check for Tx_Foo_Bar style class names
601
                if (is_object($result) && $result instanceof QueryResultInterface) {
602
                    $propertyValue = $result->getFirst();
603
                } else {
604
                    $propertyValue = $result;
605
                }
606
            }
607
        }
608
        return $propertyValue;
609
    }
610
611
    /**
612
     * Counts the number of related objects assigned to a property of a parent object
613
     *
614
     * @param DomainObjectInterface $parentObject The object instance this proxy is part of
615
     * @param string $propertyName The name of the proxied property in it's parent
616
     * @param mixed $fieldValue The raw field value.
617
     * @return int
618
     */
619
    public function countRelated(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '')
620
    {
621
        $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
622
        return $query->execute()->count();
623
    }
624
625
    /**
626
     * Returns a data map for a given class name
627
     *
628
     * @param string $className The class name you want to fetch the Data Map for
629
     * @throws Persistence\Generic\Exception
630
     * @return DataMap The data map
631
     */
632
    public function getDataMap($className)
633
    {
634
        if (!is_string($className) || $className === '') {
0 ignored issues
show
introduced by
The condition is_string($className) is always true.
Loading history...
635
            throw new Exception('No class name was given to retrieve the Data Map for.', 1251315965);
636
        }
637
        return $this->dataMapFactory->buildDataMap($className);
638
    }
639
640
    /**
641
     * Returns the selector (table) name for a given class name.
642
     *
643
     * @param string $className
644
     * @return string The selector name
645
     */
646
    public function convertClassNameToTableName($className)
647
    {
648
        return $this->getDataMap($className)->getTableName();
649
    }
650
651
    /**
652
     * Returns the column name for a given property name of the specified class.
653
     *
654
     * @param string $propertyName
655
     * @param string $className
656
     * @return string The column name
657
     */
658
    public function convertPropertyNameToColumnName($propertyName, $className = null)
659
    {
660
        if (!empty($className)) {
661
            $dataMap = $this->getDataMap($className);
662
            if ($dataMap !== null) {
663
                $columnMap = $dataMap->getColumnMap($propertyName);
664
                if ($columnMap !== null) {
665
                    return $columnMap->getColumnName();
666
                }
667
            }
668
        }
669
        return GeneralUtility::camelCaseToLowerCaseUnderscored($propertyName);
670
    }
671
672
    /**
673
     * Returns the type of a child object.
674
     *
675
     * @param string $parentClassName The class name of the object this proxy is part of
676
     * @param string $propertyName The name of the proxied property in it's parent
677
     * @throws UnexpectedTypeException
678
     * @return string The class name of the child object
679
     */
680
    public function getType($parentClassName, $propertyName)
681
    {
682
        try {
683
            $property = $this->reflectionService->getClassSchema($parentClassName)->getProperty($propertyName);
684
685
            if ($property->getElementType() !== null) {
686
                return $property->getElementType();
687
            }
688
689
            if ($property->getType() !== null) {
690
                return $property->getType();
691
            }
692
        } catch (NoSuchPropertyException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
693
        }
694
695
        throw new UnexpectedTypeException('Could not determine the child object type.', 1251315967);
696
    }
697
698
    /**
699
     * Returns a plain value, i.e. objects are flattened out if possible.
700
     * Multi value objects or arrays will be converted to a comma-separated list for use in IN SQL queries.
701
     *
702
     * @param mixed $input The value that will be converted.
703
     * @param ColumnMap $columnMap Optional column map for retrieving the date storage format.
704
     * @throws \InvalidArgumentException
705
     * @throws UnexpectedTypeException
706
     * @return int|string
707
     */
708
    public function getPlainValue($input, $columnMap = null)
709
    {
710
        if ($input === null) {
711
            return 'NULL';
712
        }
713
        if ($input instanceof LazyLoadingProxy) {
714
            $input = $input->_loadRealInstance();
715
        }
716
717
        if (is_bool($input)) {
718
            $parameter = (int)$input;
719
        } elseif (is_int($input)) {
720
            $parameter = $input;
721
        } elseif ($input instanceof \DateTimeInterface) {
722
            if ($columnMap !== null && $columnMap->getDateTimeStorageFormat() !== null) {
723
                $storageFormat = $columnMap->getDateTimeStorageFormat();
724
                switch ($storageFormat) {
725
                    case 'datetime':
726
                        $parameter = $input->format('Y-m-d H:i:s');
727
                        break;
728
                    case 'date':
729
                        $parameter = $input->format('Y-m-d');
730
                        break;
731
                    case 'time':
732
                        $parameter = $input->format('H:i');
733
                        break;
734
                    default:
735
                        throw new \InvalidArgumentException('Column map DateTime format "' . $storageFormat . '" is unknown. Allowed values are date, datetime or time.', 1395353470);
736
                }
737
            } else {
738
                $parameter = $input->format('U');
739
            }
740
        } elseif ($input instanceof DomainObjectInterface) {
741
            $parameter = (int)$input->getUid();
742
        } elseif (TypeHandlingUtility::isValidTypeForMultiValueComparison($input)) {
743
            $plainValueArray = [];
744
            foreach ($input as $inputElement) {
745
                $plainValueArray[] = $this->getPlainValue($inputElement, $columnMap);
746
            }
747
            $parameter = implode(',', $plainValueArray);
748
        } elseif (is_object($input)) {
749
            if (TypeHandlingUtility::isCoreType($input)) {
750
                $parameter = (string)$input;
751
            } else {
752
                throw new UnexpectedTypeException('An object of class "' . get_class($input) . '" could not be converted to a plain value.', 1274799934);
753
            }
754
        } else {
755
            $parameter = (string)$input;
756
        }
757
        return $parameter;
758
    }
759
}
760