Backend::getObjectByIdentifier()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 10
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;
17
18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use TYPO3\CMS\Core\Database\ReferenceIndex;
20
use TYPO3\CMS\Core\SingletonInterface;
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
22
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
23
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
24
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
25
use TYPO3\CMS\Extbase\Event\Persistence\EntityAddedToPersistenceEvent;
26
use TYPO3\CMS\Extbase\Event\Persistence\EntityFinalizedAfterPersistenceEvent;
27
use TYPO3\CMS\Extbase\Event\Persistence\EntityPersistedEvent;
28
use TYPO3\CMS\Extbase\Event\Persistence\EntityRemovedFromPersistenceEvent;
29
use TYPO3\CMS\Extbase\Event\Persistence\EntityUpdatedInPersistenceEvent;
30
use TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectDataEvent;
31
use TYPO3\CMS\Extbase\Event\Persistence\ModifyResultAfterFetchingObjectDataEvent;
32
use TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException;
33
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
34
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
35
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
36
use TYPO3\CMS\Extbase\Persistence\ObjectMonitoringInterface;
37
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
38
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
39
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
40
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
41
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
42
43
/**
44
 * A persistence backend. This backend maps objects to the relational model of the storage backend.
45
 * It persists all added, removed and changed objects.
46
 * @internal only to be used within Extbase, not part of TYPO3 Core API.
47
 */
48
class Backend implements BackendInterface, SingletonInterface
49
{
50
    /**
51
     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Session
52
     */
53
    protected $session;
54
55
    /**
56
     * @var \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface
57
     */
58
    protected $persistenceManager;
59
60
    /**
61
     * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
62
     */
63
    protected $aggregateRootObjects;
64
65
    /**
66
     * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
67
     */
68
    protected $deletedEntities;
69
70
    /**
71
     * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
72
     */
73
    protected $changedEntities;
74
75
    /**
76
     * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
77
     */
78
    protected $visitedDuringPersistence;
79
80
    /**
81
     * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
82
     */
83
    protected $reflectionService;
84
85
    /**
86
     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface
87
     */
88
    protected $storageBackend;
89
90
    /**
91
     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory
92
     */
93
    protected $dataMapFactory;
94
95
    /**
96
     * The TYPO3 reference index object
97
     *
98
     * @var \TYPO3\CMS\Core\Database\ReferenceIndex
99
     */
100
    protected $referenceIndex;
101
102
    /**
103
     * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
104
     */
105
    protected $configurationManager;
106
107
    /**
108
     * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
109
     */
110
    protected $signalSlotDispatcher;
111
112
    /**
113
     * @var EventDispatcherInterface
114
     */
115
    protected $eventDispatcher;
116
117
    /**
118
     * Constructs the backend
119
     *
120
     * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
121
     * @param Session $session
122
     * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
123
     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface $storageBackend
124
     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory $dataMapFactory
125
     * @param EventDispatcherInterface $eventDispatcher
126
     */
127
    public function __construct(
128
        ConfigurationManagerInterface $configurationManager,
129
        Session $session,
130
        ReflectionService $reflectionService,
131
        \TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface $storageBackend,
132
        DataMapFactory $dataMapFactory,
133
        EventDispatcherInterface $eventDispatcher
134
    ) {
135
        $this->configurationManager = $configurationManager;
136
        $this->session = $session;
137
        $this->reflectionService = $reflectionService;
138
        $this->storageBackend = $storageBackend;
139
        $this->dataMapFactory = $dataMapFactory;
140
        $this->eventDispatcher = $eventDispatcher;
141
142
        $this->referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
143
        $this->aggregateRootObjects = new ObjectStorage();
144
        $this->deletedEntities = new ObjectStorage();
145
        $this->changedEntities = new ObjectStorage();
146
    }
147
148
    /**
149
     * @param \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager
150
     */
151
    public function setPersistenceManager(PersistenceManagerInterface $persistenceManager)
152
    {
153
        $this->persistenceManager = $persistenceManager;
154
    }
155
156
    /**
157
     * Returns the number of records matching the query.
158
     *
159
     * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
160
     * @return int
161
     */
162
    public function getObjectCountByQuery(QueryInterface $query)
163
    {
164
        return $this->storageBackend->getObjectCountByQuery($query);
165
    }
166
167
    /**
168
     * Returns the object data matching the $query.
169
     *
170
     * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
171
     * @return array
172
     */
173
    public function getObjectDataByQuery(QueryInterface $query)
174
    {
175
        $event = new ModifyQueryBeforeFetchingObjectDataEvent($query);
176
        $this->eventDispatcher->dispatch($event);
177
        $query = $event->getQuery();
178
        $result = $this->storageBackend->getObjectDataByQuery($query);
179
        $event = new ModifyResultAfterFetchingObjectDataEvent($query, $result);
180
        $this->eventDispatcher->dispatch($event);
181
        return $event->getResult();
182
    }
183
184
    /**
185
     * Returns the (internal) identifier for the object, if it is known to the
186
     * backend. Otherwise NULL is returned.
187
     *
188
     * @param object $object
189
     * @return string|null The identifier for the object if it is known, or NULL
190
     */
191
    public function getIdentifierByObject($object)
192
    {
193
        if ($object instanceof LazyLoadingProxy) {
194
            $object = $object->_loadRealInstance();
195
            if (!is_object($object)) {
196
                return null;
197
            }
198
        }
199
        return $this->session->getIdentifierByObject($object);
200
    }
201
202
    /**
203
     * Returns the object with the (internal) identifier, if it is known to the
204
     * backend. Otherwise NULL is returned.
205
     *
206
     * @param string $identifier
207
     * @param string $className
208
     * @return object|null The object for the identifier if it is known, or NULL
209
     */
210
    public function getObjectByIdentifier($identifier, $className)
211
    {
212
        if ($this->session->hasIdentifier($identifier, $className)) {
213
            return $this->session->getObjectByIdentifier($identifier, $className);
214
        }
215
        $query = $this->persistenceManager->createQueryForType($className);
216
        $query->getQuerySettings()->setRespectStoragePage(false);
217
        $query->getQuerySettings()->setRespectSysLanguage(false);
218
        $query->getQuerySettings()->setLanguageOverlayMode(true);
219
        return $query->matching($query->equals('uid', $identifier))->execute()->getFirst();
220
    }
221
222
    /**
223
     * Checks if the given object has ever been persisted.
224
     *
225
     * @param object $object The object to check
226
     * @return bool TRUE if the object is new, FALSE if the object exists in the repository
227
     */
228
    public function isNewObject($object)
229
    {
230
        return $this->getIdentifierByObject($object) === null;
231
    }
232
233
    /**
234
     * Sets the aggregate root objects
235
     *
236
     * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objects
237
     */
238
    public function setAggregateRootObjects(ObjectStorage $objects)
239
    {
240
        $this->aggregateRootObjects = $objects;
241
    }
242
243
    /**
244
     * Sets the changed objects
245
     *
246
     * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $entities
247
     */
248
    public function setChangedEntities(ObjectStorage $entities)
249
    {
250
        $this->changedEntities = $entities;
251
    }
252
253
    /**
254
     * Sets the deleted objects
255
     *
256
     * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $entities
257
     */
258
    public function setDeletedEntities(ObjectStorage $entities)
259
    {
260
        $this->deletedEntities = $entities;
261
    }
262
263
    /**
264
     * Commits the current persistence session.
265
     */
266
    public function commit()
267
    {
268
        $this->persistObjects();
269
        $this->processDeletedObjects();
270
    }
271
272
    /**
273
     * Traverse and persist all aggregate roots and their object graph.
274
     */
275
    protected function persistObjects()
276
    {
277
        $this->visitedDuringPersistence = new ObjectStorage();
278
        foreach ($this->aggregateRootObjects as $object) {
279
            /** @var DomainObjectInterface $object */
280
            if ($object->_isNew()) {
281
                $this->insertObject($object);
282
            }
283
            $this->persistObject($object);
284
        }
285
        foreach ($this->changedEntities as $object) {
286
            $this->persistObject($object);
287
        }
288
    }
289
290
    /**
291
     * Persists the given object.
292
     *
293
     * @param DomainObjectInterface $object The object to be inserted
294
     */
295
    protected function persistObject(DomainObjectInterface $object)
296
    {
297
        if (isset($this->visitedDuringPersistence[$object])) {
298
            return;
299
        }
300
        $row = [];
301
        $queue = [];
302
        $dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
303
        $properties = $object->_getProperties();
304
        foreach ($properties as $propertyName => $propertyValue) {
305
            if (!$dataMap->isPersistableProperty($propertyName) || $this->propertyValueIsLazyLoaded($propertyValue)) {
306
                continue;
307
            }
308
            $columnMap = $dataMap->getColumnMap($propertyName);
309
            if ($propertyValue instanceof ObjectStorage) {
310
                $cleanProperty = $object->_getCleanProperty($propertyName);
311
                // objectstorage needs to be persisted if the object is new, the objectstorage is dirty, meaning it has
312
                // been changed after initial build, or an empty objectstorage is present and the cleanstate objectstorage
313
                // has childelements, meaning all elements should been removed from the objectstorage
314
                if ($object->_isNew() || $propertyValue->_isDirty() || ($propertyValue->count() === 0 && $cleanProperty && $cleanProperty->count() > 0)) {
315
                    $this->persistObjectStorage($propertyValue, $object, $propertyName, $row);
316
                    $propertyValue->_memorizeCleanState();
317
                }
318
                foreach ($propertyValue as $containedObject) {
319
                    if ($containedObject instanceof DomainObjectInterface) {
320
                        $queue[] = $containedObject;
321
                    }
322
                }
323
            } elseif ($propertyValue instanceof DomainObjectInterface
324
                && $object instanceof ObjectMonitoringInterface) {
325
                if ($object->_isDirty($propertyName)) {
0 ignored issues
show
Unused Code introduced by
The call to TYPO3\CMS\Extbase\Persis...ngInterface::_isDirty() has too many arguments starting with $propertyName. ( Ignorable by Annotation )

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

325
                if ($object->/** @scrutinizer ignore-call */ _isDirty($propertyName)) {

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...
326
                    if ($propertyValue->_isNew()) {
327
                        $this->insertObject($propertyValue, $object, $propertyName);
328
                    }
329
                    $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue);
330
                }
331
                $queue[] = $propertyValue;
332
            } elseif ($object->_isNew() || $object->_isDirty($propertyName)) {
0 ignored issues
show
Bug introduced by
The method _isDirty() 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

332
            } elseif ($object->_isNew() || $object->/** @scrutinizer ignore-call */ _isDirty($propertyName)) {
Loading history...
333
                $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue, $columnMap);
334
            }
335
        }
336
        if (!empty($row)) {
337
            $this->updateObject($object, $row);
338
            $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

338
            $object->/** @scrutinizer ignore-call */ 
339
                     _memorizeCleanState();
Loading history...
339
        }
340
        $this->visitedDuringPersistence[$object] = $object->getUid();
341
        foreach ($queue as $queuedObject) {
342
            $this->persistObject($queuedObject);
343
        }
344
        $this->eventDispatcher->dispatch(new EntityPersistedEvent($object));
345
    }
346
347
    /**
348
     * Checks, if the property value is lazy loaded and was not initialized
349
     *
350
     * @param mixed $propertyValue The property value
351
     * @return bool
352
     */
353
    protected function propertyValueIsLazyLoaded($propertyValue)
354
    {
355
        if ($propertyValue instanceof LazyLoadingProxy) {
356
            return true;
357
        }
358
        if ($propertyValue instanceof LazyObjectStorage) {
359
            if ($propertyValue->isInitialized() === false) {
360
                return true;
361
            }
362
        }
363
        return false;
364
    }
365
366
    /**
367
     * Persists an object storage. Objects of a 1:n or m:n relation are queued and processed with the parent object.
368
     * A 1:1 relation gets persisted immediately. Objects which were removed from the property were detached from
369
     * the parent object. They will not be deleted by default. You have to annotate the property
370
     * with '@TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove")' if you want them to be deleted as well.
371
     *
372
     * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objectStorage The object storage to be persisted.
373
     * @param DomainObjectInterface $parentObject The parent object. One of the properties holds the object storage.
374
     * @param string $propertyName The name of the property holding the object storage.
375
     * @param array $row The row array of the parent object to be persisted. It's passed by reference and gets filled with either a comma separated list of uids (csv) or the number of contained objects.
376
     */
377
    protected function persistObjectStorage(ObjectStorage $objectStorage, DomainObjectInterface $parentObject, $propertyName, array &$row)
378
    {
379
        $className = get_class($parentObject);
380
        $dataMapper = GeneralUtility::makeInstance(DataMapper::class);
381
        $columnMap = $this->dataMapFactory->buildDataMap($className)->getColumnMap($propertyName);
382
        $property = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
383
        foreach ($this->getRemovedChildObjects($parentObject, $propertyName) as $removedObject) {
384
            $this->detachObjectFromParentObject($removedObject, $parentObject, $propertyName);
385
            if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY && $property->getCascadeValue() === 'remove') {
386
                $this->removeEntity($removedObject);
387
            }
388
        }
389
390
        $currentUids = [];
391
        $sortingPosition = 1;
392
        $updateSortingOfFollowing = false;
393
394
        foreach ($objectStorage as $object) {
395
            /** @var DomainObjectInterface $object */
396
            if (empty($currentUids)) {
397
                $sortingPosition = 1;
398
            } else {
399
                $sortingPosition++;
400
            }
401
            $cleanProperty = $parentObject->_getCleanProperty($propertyName);
402
            if ($object->_isNew()) {
403
                $this->insertObject($object);
404
                $this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
405
                // if a new object is inserted, all objects after this need to have their sorting updated
406
                $updateSortingOfFollowing = true;
407
            } elseif ($cleanProperty === null || $cleanProperty->getPosition($object) === null) {
408
                // if parent object is new then it doesn't have cleanProperty yet; before attaching object it's clean position is null
409
                $this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
410
                // if a relation is dirty (speaking the same object is removed and added again at a different position), all objects after this needs to be updated the sorting
411
                $updateSortingOfFollowing = true;
412
            } elseif ($objectStorage->isRelationDirty($object) || $cleanProperty->getPosition($object) !== $objectStorage->getPosition($object)) {
413
                $this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
414
                $updateSortingOfFollowing = true;
415
            } elseif ($updateSortingOfFollowing) {
416
                if ($sortingPosition > $objectStorage->getPosition($object)) {
417
                    $this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
418
                } else {
419
                    $sortingPosition = $objectStorage->getPosition($object);
420
                }
421
            }
422
            $currentUids[] = $object->getUid();
423
        }
424
425
        if ($columnMap->getParentKeyFieldName() === null) {
426
            $row[$columnMap->getColumnName()] = implode(',', $currentUids);
427
        } else {
428
            $row[$columnMap->getColumnName()] = $dataMapper->countRelated($parentObject, $propertyName);
429
        }
430
    }
431
432
    /**
433
     * Returns the removed objects determined by a comparison of the clean property value
434
     * with the actual property value.
435
     *
436
     * @param DomainObjectInterface $object The object
437
     * @param string $propertyName
438
     * @return array An array of removed objects
439
     */
440
    protected function getRemovedChildObjects(DomainObjectInterface $object, $propertyName)
441
    {
442
        $removedObjects = [];
443
        $cleanPropertyValue = $object->_getCleanProperty($propertyName);
444
        if (is_array($cleanPropertyValue) || $cleanPropertyValue instanceof \Iterator) {
445
            $propertyValue = $object->_getProperty($propertyName);
446
            foreach ($cleanPropertyValue as $containedObject) {
447
                if (!$propertyValue->contains($containedObject)) {
448
                    $removedObjects[] = $containedObject;
449
                }
450
            }
451
        }
452
        return $removedObjects;
453
    }
454
455
    /**
456
     * Updates the fields defining the relation between the object and the parent object.
457
     *
458
     * @param DomainObjectInterface $object
459
     * @param DomainObjectInterface $parentObject
460
     * @param string $parentPropertyName
461
     * @param int $sortingPosition
462
     */
463
    protected function attachObjectToParentObject(DomainObjectInterface $object, DomainObjectInterface $parentObject, $parentPropertyName, $sortingPosition = 0)
464
    {
465
        $parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
466
467
        $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
468
        if ($parentColumnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
469
            $this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition);
470
        } elseif ($parentColumnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
471
            $this->insertRelationInRelationtable($object, $parentObject, $parentPropertyName, $sortingPosition);
472
        }
473
    }
474
475
    /**
476
     * Updates the fields defining the relation between the object and the parent object.
477
     *
478
     * @param DomainObjectInterface $object
479
     * @param DomainObjectInterface $parentObject
480
     * @param string $parentPropertyName
481
     * @param int $sortingPosition
482
     */
483
    protected function updateRelationOfObjectToParentObject(DomainObjectInterface $object, DomainObjectInterface $parentObject, $parentPropertyName, $sortingPosition = 0)
484
    {
485
        $parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
486
        $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
487
        if ($parentColumnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
488
            $this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition);
489
        } elseif ($parentColumnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
490
            $this->updateRelationInRelationTable($object, $parentObject, $parentPropertyName, $sortingPosition);
491
        }
492
    }
493
494
    /**
495
     * Updates fields defining the relation between the object and the parent object in relation has-many.
496
     *
497
     * @param DomainObjectInterface $object
498
     * @param DomainObjectInterface $parentObject
499
     * @param string $parentPropertyName
500
     * @param int $sortingPosition
501
     * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException
502
     */
503
    protected function attachObjectToParentObjectRelationHasMany(DomainObjectInterface $object, DomainObjectInterface $parentObject, $parentPropertyName, $sortingPosition = 0)
504
    {
505
        $parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
506
        $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
507
        if ($parentColumnMap->getTypeOfRelation() !== ColumnMap::RELATION_HAS_MANY) {
508
            throw new IllegalRelationTypeException(
509
                'Parent column relation type is ' . $parentColumnMap->getTypeOfRelation() .
510
                ' but should be ' . ColumnMap::RELATION_HAS_MANY,
511
                1345368105
512
            );
513
        }
514
        $row = [];
515
        $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
516
        if ($parentKeyFieldName !== null) {
517
            $row[$parentKeyFieldName] = $parentObject->_getProperty('_localizedUid') ?: $parentObject->getUid();
518
            $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
519
            if ($parentTableFieldName !== null) {
520
                $row[$parentTableFieldName] = $parentDataMap->getTableName();
521
            }
522
            $relationTableMatchFields = $parentColumnMap->getRelationTableMatchFields();
523
            if (is_array($relationTableMatchFields)) {
524
                $row = array_merge($relationTableMatchFields, $row);
525
            }
526
        }
527
        $childSortByFieldName = $parentColumnMap->getChildSortByFieldName();
528
        if (!empty($childSortByFieldName)) {
529
            $row[$childSortByFieldName] = $sortingPosition;
530
        }
531
        if (!empty($row)) {
532
            $this->updateObject($object, $row);
533
        }
534
    }
535
536
    /**
537
     * Updates the fields defining the relation between the object and the parent object.
538
     *
539
     * @param DomainObjectInterface $object
540
     * @param DomainObjectInterface $parentObject
541
     * @param string $parentPropertyName
542
     */
543
    protected function detachObjectFromParentObject(DomainObjectInterface $object, DomainObjectInterface $parentObject, $parentPropertyName)
544
    {
545
        $parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
546
        $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
547
        if ($parentColumnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
548
            $row = [];
549
            $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
550
            if ($parentKeyFieldName !== null) {
551
                $row[$parentKeyFieldName] = 0;
552
                $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
553
                if ($parentTableFieldName !== null) {
554
                    $row[$parentTableFieldName] = '';
555
                }
556
                $relationTableMatchFields = $parentColumnMap->getRelationTableMatchFields();
557
                if (is_array($relationTableMatchFields) && !empty($relationTableMatchFields)) {
558
                    $row = array_merge(array_fill_keys(array_keys($relationTableMatchFields), ''), $row);
559
                }
560
            }
561
            $childSortByFieldName = $parentColumnMap->getChildSortByFieldName();
562
            if (!empty($childSortByFieldName)) {
563
                $row[$childSortByFieldName] = 0;
564
            }
565
            if (!empty($row)) {
566
                $this->updateObject($object, $row);
567
            }
568
        } elseif ($parentColumnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
569
            $this->deleteRelationFromRelationtable($object, $parentObject, $parentPropertyName);
570
        }
571
    }
572
573
    /**
574
     * Inserts an object in the storage backend
575
     *
576
     * @param DomainObjectInterface $object The object to be inserted in the storage
577
     * @param DomainObjectInterface $parentObject The parentobject.
578
     * @param string $parentPropertyName
579
     */
580
    protected function insertObject(DomainObjectInterface $object, DomainObjectInterface $parentObject = null, $parentPropertyName = '')
581
    {
582
        if ($object instanceof AbstractValueObject) {
583
            $result = $this->getUidOfAlreadyPersistedValueObject($object);
584
            if ($result !== null) {
585
                $object->_setProperty('uid', $result);
586
                return;
587
            }
588
        }
589
        $dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
590
        $row = [];
591
        $properties = $object->_getProperties();
592
        foreach ($properties as $propertyName => $propertyValue) {
593
            if (!$dataMap->isPersistableProperty($propertyName) || $this->propertyValueIsLazyLoaded($propertyValue)) {
594
                continue;
595
            }
596
            $columnMap = $dataMap->getColumnMap($propertyName);
597
            if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_ONE) {
598
                $row[$columnMap->getColumnName()] = 0;
599
            } elseif ($columnMap->getTypeOfRelation() !== ColumnMap::RELATION_NONE) {
600
                if ($columnMap->getParentKeyFieldName() === null) {
601
                    // CSV type relation
602
                    $row[$columnMap->getColumnName()] = '';
603
                } else {
604
                    // MM type relation
605
                    $row[$columnMap->getColumnName()] = 0;
606
                }
607
            } elseif ($propertyValue !== null) {
608
                $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue, $columnMap);
609
            }
610
        }
611
        $this->addCommonFieldsToRow($object, $row);
612
        if ($dataMap->getLanguageIdColumnName() !== null && $object->_getProperty('_languageUid') === null) {
613
            $row[$dataMap->getLanguageIdColumnName()] = 0;
614
            $object->_setProperty('_languageUid', 0);
615
        }
616
        if ($dataMap->getTranslationOriginColumnName() !== null) {
0 ignored issues
show
introduced by
The condition $dataMap->getTranslation...inColumnName() !== null is always true.
Loading history...
617
            $row[$dataMap->getTranslationOriginColumnName()] = 0;
618
        }
619
        if ($dataMap->getTranslationOriginDiffSourceName() !== null) {
0 ignored issues
show
introduced by
The condition $dataMap->getTranslation...ffSourceName() !== null is always true.
Loading history...
620
            $row[$dataMap->getTranslationOriginDiffSourceName()] = '';
621
        }
622
        if ($parentObject !== null && $parentPropertyName) {
623
            $parentColumnDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject))->getColumnMap($parentPropertyName);
624
            $relationTableMatchFields = $parentColumnDataMap->getRelationTableMatchFields();
625
            if (is_array($relationTableMatchFields)) {
626
                $row = array_merge($relationTableMatchFields, $row);
627
            }
628
            if ($parentColumnDataMap->getParentKeyFieldName() !== null) {
629
                $row[$parentColumnDataMap->getParentKeyFieldName()] = (int)$parentObject->getUid();
630
            }
631
        }
632
        $uid = $this->storageBackend->addRow($dataMap->getTableName(), $row);
633
        $object->_setProperty('uid', (int)$uid);
634
        $object->setPid((int)$row['pid']);
635
        if ((int)$uid >= 1) {
636
            $this->eventDispatcher->dispatch(new EntityAddedToPersistenceEvent($object));
637
        }
638
        $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
639
        if ($frameworkConfiguration['persistence']['updateReferenceIndex'] === '1') {
640
            $this->referenceIndex->updateRefIndexTable($dataMap->getTableName(), $uid);
641
        }
642
        $this->session->registerObject($object, $uid);
643
        if ((int)$uid >= 1) {
644
            $this->eventDispatcher->dispatch(new EntityFinalizedAfterPersistenceEvent($object));
645
        }
646
    }
647
648
    /**
649
     * Tests, if the given Value Object already exists in the storage backend and if so, it returns the uid.
650
     *
651
     * @param \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object The object to be tested
652
     * @return int|null The matching uid if an object was found, else null
653
     */
654
    protected function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object)
655
    {
656
        return $this->storageBackend->getUidOfAlreadyPersistedValueObject($object);
657
    }
658
659
    /**
660
     * Inserts mm-relation into a relation table
661
     *
662
     * @param DomainObjectInterface $object The related object
663
     * @param DomainObjectInterface $parentObject The parent object
664
     * @param string $propertyName The name of the parent object's property where the related objects are stored in
665
     * @param int $sortingPosition Defaults to NULL
666
     * @return int The uid of the inserted row
667
     */
668
    protected function insertRelationInRelationtable(DomainObjectInterface $object, DomainObjectInterface $parentObject, $propertyName, $sortingPosition = null)
669
    {
670
        $dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
671
        $columnMap = $dataMap->getColumnMap($propertyName);
672
        $parentUid = $parentObject->getUid();
673
        if ($parentObject->_getProperty('_localizedUid') !== null) {
674
            $parentUid = $parentObject->_getProperty('_localizedUid');
675
        }
676
        $row = [
677
            $columnMap->getParentKeyFieldName() => (int)$parentUid,
678
            $columnMap->getChildKeyFieldName() => (int)$object->getUid(),
679
            $columnMap->getChildSortByFieldName() => $sortingPosition !== null ? (int)$sortingPosition : 0
680
        ];
681
        $relationTableName = $columnMap->getRelationTableName();
682
        if ($columnMap->getRelationTablePageIdColumnName() !== null) {
683
            $row[$columnMap->getRelationTablePageIdColumnName()] = $this->determineStoragePageIdForNewRecord();
684
        }
685
        $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
686
        if (is_array($relationTableMatchFields)) {
687
            $row = array_merge($relationTableMatchFields, $row);
688
        }
689
        $relationTableInsertFields = $columnMap->getRelationTableInsertFields();
690
        if (is_array($relationTableInsertFields)) {
691
            $row = array_merge($relationTableInsertFields, $row);
692
        }
693
        $res = $this->storageBackend->addRow($relationTableName, $row, true);
0 ignored issues
show
Bug introduced by
It seems like $relationTableName can also be of type null; however, parameter $tableName of TYPO3\CMS\Extbase\Persis...kendInterface::addRow() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

693
        $res = $this->storageBackend->addRow(/** @scrutinizer ignore-type */ $relationTableName, $row, true);
Loading history...
694
        return $res;
695
    }
696
697
    /**
698
     * Updates mm-relation in a relation table
699
     *
700
     * @param DomainObjectInterface $object The related object
701
     * @param DomainObjectInterface $parentObject The parent object
702
     * @param string $propertyName The name of the parent object's property where the related objects are stored in
703
     * @param int $sortingPosition Defaults to NULL
704
     * @return bool TRUE if update was successfully
705
     */
706
    protected function updateRelationInRelationTable(DomainObjectInterface $object, DomainObjectInterface $parentObject, $propertyName, $sortingPosition = 0)
707
    {
708
        $dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
709
        $columnMap = $dataMap->getColumnMap($propertyName);
710
        $row = [
711
            $columnMap->getParentKeyFieldName() => (int)$parentObject->getUid(),
712
            $columnMap->getChildKeyFieldName() => (int)$object->getUid(),
713
            $columnMap->getChildSortByFieldName() => (int)$sortingPosition
714
        ];
715
        $relationTableName = $columnMap->getRelationTableName();
716
        $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
717
        if (is_array($relationTableMatchFields)) {
718
            $row = array_merge($relationTableMatchFields, $row);
719
        }
720
        $this->storageBackend->updateRelationTableRow(
721
            $relationTableName,
0 ignored issues
show
Bug introduced by
It seems like $relationTableName can also be of type null; however, parameter $tableName of TYPO3\CMS\Extbase\Persis...pdateRelationTableRow() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

721
            /** @scrutinizer ignore-type */ $relationTableName,
Loading history...
722
            $row
723
        );
724
        return true;
725
    }
726
727
    /**
728
     * Delete all mm-relations of a parent from a relation table
729
     *
730
     * @param DomainObjectInterface $parentObject The parent object
731
     * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
732
     * @return bool TRUE if delete was successfully
733
     */
734
    protected function deleteAllRelationsFromRelationtable(DomainObjectInterface $parentObject, $parentPropertyName)
735
    {
736
        $dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
737
        $columnMap = $dataMap->getColumnMap($parentPropertyName);
738
        $relationTableName = $columnMap->getRelationTableName();
739
        $relationMatchFields = [
740
            $columnMap->getParentKeyFieldName() => (int)$parentObject->getUid()
741
        ];
742
        $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
743
        if (is_array($relationTableMatchFields)) {
744
            $relationMatchFields = array_merge($relationTableMatchFields, $relationMatchFields);
745
        }
746
        $this->storageBackend->removeRow($relationTableName, $relationMatchFields, false);
0 ignored issues
show
Bug introduced by
It seems like $relationTableName can also be of type null; however, parameter $tableName of TYPO3\CMS\Extbase\Persis...dInterface::removeRow() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

746
        $this->storageBackend->removeRow(/** @scrutinizer ignore-type */ $relationTableName, $relationMatchFields, false);
Loading history...
747
        return true;
748
    }
749
750
    /**
751
     * Delete an mm-relation from a relation table
752
     *
753
     * @param DomainObjectInterface $relatedObject The related object
754
     * @param DomainObjectInterface $parentObject The parent object
755
     * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
756
     * @return bool
757
     */
758
    protected function deleteRelationFromRelationtable(DomainObjectInterface $relatedObject, DomainObjectInterface $parentObject, $parentPropertyName)
759
    {
760
        $dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
761
        $columnMap = $dataMap->getColumnMap($parentPropertyName);
762
        $relationTableName = $columnMap->getRelationTableName();
763
        $relationMatchFields = [
764
            $columnMap->getParentKeyFieldName() => (int)$parentObject->getUid(),
765
            $columnMap->getChildKeyFieldName() => (int)$relatedObject->getUid()
766
        ];
767
        $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
768
        if (is_array($relationTableMatchFields)) {
769
            $relationMatchFields = array_merge($relationTableMatchFields, $relationMatchFields);
770
        }
771
        $this->storageBackend->removeRow($relationTableName, $relationMatchFields, false);
0 ignored issues
show
Bug introduced by
It seems like $relationTableName can also be of type null; however, parameter $tableName of TYPO3\CMS\Extbase\Persis...dInterface::removeRow() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

771
        $this->storageBackend->removeRow(/** @scrutinizer ignore-type */ $relationTableName, $relationMatchFields, false);
Loading history...
772
        return true;
773
    }
774
775
    /**
776
     * Updates a given object in the storage
777
     *
778
     * @param DomainObjectInterface $object The object to be updated
779
     * @param array $row Row to be stored
780
     * @return bool
781
     */
782
    protected function updateObject(DomainObjectInterface $object, array $row)
783
    {
784
        $dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
785
        $this->addCommonFieldsToRow($object, $row);
786
        $row['uid'] = $object->getUid();
787
        if ($dataMap->getLanguageIdColumnName() !== null) {
0 ignored issues
show
introduced by
The condition $dataMap->getLanguageIdColumnName() !== null is always true.
Loading history...
788
            $row[$dataMap->getLanguageIdColumnName()] = (int)$object->_getProperty('_languageUid');
789
            if ($object->_getProperty('_localizedUid') !== null) {
790
                $row['uid'] = $object->_getProperty('_localizedUid');
791
            }
792
        }
793
        $this->storageBackend->updateRow($dataMap->getTableName(), $row);
794
        $this->eventDispatcher->dispatch(new EntityUpdatedInPersistenceEvent($object));
795
796
        $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
797
        if ($frameworkConfiguration['persistence']['updateReferenceIndex'] === '1') {
798
            $this->referenceIndex->updateRefIndexTable($dataMap->getTableName(), $row['uid']);
799
        }
800
        return true;
801
    }
802
803
    /**
804
     * Adds common database fields to a row
805
     *
806
     * @param DomainObjectInterface $object
807
     * @param array $row
808
     */
809
    protected function addCommonFieldsToRow(DomainObjectInterface $object, array &$row)
810
    {
811
        $dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
812
        $this->addCommonDateFieldsToRow($object, $row);
813
        if ($dataMap->getRecordTypeColumnName() !== null && $dataMap->getRecordType() !== null) {
814
            $row[$dataMap->getRecordTypeColumnName()] = $dataMap->getRecordType();
815
        }
816
        if ($object->_isNew() && !isset($row['pid'])) {
817
            $row['pid'] = $this->determineStoragePageIdForNewRecord($object);
818
        }
819
    }
820
821
    /**
822
     * Adjusts the common date fields of the given row to the current time
823
     *
824
     * @param DomainObjectInterface $object
825
     * @param array $row The row to be updated
826
     */
827
    protected function addCommonDateFieldsToRow(DomainObjectInterface $object, array &$row)
828
    {
829
        $dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
830
        if ($object->_isNew() && $dataMap->getCreationDateColumnName() !== null) {
831
            $row[$dataMap->getCreationDateColumnName()] = $GLOBALS['EXEC_TIME'];
832
        }
833
        if ($dataMap->getModificationDateColumnName() !== null) {
0 ignored issues
show
introduced by
The condition $dataMap->getModificatio...teColumnName() !== null is always true.
Loading history...
834
            $row[$dataMap->getModificationDateColumnName()] = $GLOBALS['EXEC_TIME'];
835
        }
836
    }
837
838
    /**
839
     * Iterate over deleted aggregate root objects and process them
840
     */
841
    protected function processDeletedObjects()
842
    {
843
        foreach ($this->deletedEntities as $entity) {
844
            if ($this->session->hasObject($entity)) {
845
                $this->removeEntity($entity);
846
                $this->session->unregisterReconstitutedEntity($entity);
847
                $this->session->unregisterObject($entity);
848
            }
849
        }
850
        $this->deletedEntities = new ObjectStorage();
851
    }
852
853
    /**
854
     * Deletes an object
855
     *
856
     * @param DomainObjectInterface $object The object to be removed from the storage
857
     * @param bool $markAsDeleted Whether to just flag the row deleted (default) or really delete it
858
     */
859
    protected function removeEntity(DomainObjectInterface $object, $markAsDeleted = true)
860
    {
861
        $dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
862
        $tableName = $dataMap->getTableName();
863
        if ($markAsDeleted === true && $dataMap->getDeletedFlagColumnName() !== null) {
864
            $deletedColumnName = $dataMap->getDeletedFlagColumnName();
865
            $row = [
866
                'uid' => $object->getUid(),
867
                $deletedColumnName => 1
868
            ];
869
            $this->addCommonDateFieldsToRow($object, $row);
870
            $this->storageBackend->updateRow($tableName, $row);
871
        } else {
872
            $this->storageBackend->removeRow($tableName, ['uid' => $object->getUid()]);
873
        }
874
        $this->eventDispatcher->dispatch(new EntityRemovedFromPersistenceEvent($object));
875
876
        $this->removeRelatedObjects($object);
877
        $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
878
        if ($frameworkConfiguration['persistence']['updateReferenceIndex'] === '1') {
879
            $this->referenceIndex->updateRefIndexTable($tableName, $object->getUid());
880
        }
881
    }
882
883
    /**
884
     * Remove related objects
885
     *
886
     * @param DomainObjectInterface $object The object to scanned for related objects
887
     */
888
    protected function removeRelatedObjects(DomainObjectInterface $object)
889
    {
890
        $className = get_class($object);
891
        $dataMap = $this->dataMapFactory->buildDataMap($className);
892
        $classSchema = $this->reflectionService->getClassSchema($className);
893
        $properties = $object->_getProperties();
894
        foreach ($properties as $propertyName => $propertyValue) {
895
            $columnMap = $dataMap->getColumnMap($propertyName);
896
            if ($columnMap === null) {
897
                continue;
898
            }
899
            $property = $classSchema->getProperty($propertyName);
900
            if ($property->getCascadeValue() === 'remove') {
901
                if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
902
                    foreach ($propertyValue as $containedObject) {
903
                        $this->removeEntity($containedObject);
904
                    }
905
                } elseif ($propertyValue instanceof DomainObjectInterface) {
906
                    $this->removeEntity($propertyValue);
907
                }
908
            } elseif ($dataMap->getDeletedFlagColumnName() === null
909
                && $columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY
910
            ) {
911
                $this->deleteAllRelationsFromRelationtable($object, $propertyName);
912
            }
913
        }
914
    }
915
916
    /**
917
     * Determine the storage page ID for a given NEW record
918
     *
919
     * This does the following:
920
     * - If the domain object has an accessible property 'pid' (i.e. through a getPid() method), that is used to store the record.
921
     * - If there is a TypoScript configuration "classes.CLASSNAME.newRecordStoragePid", that is used to store new records.
922
     * - If there is no such TypoScript configuration, it uses the first value of The "storagePid" taken for reading records.
923
     *
924
     * @param DomainObjectInterface|null $object
925
     * @return int the storage Page ID where the object should be stored
926
     */
927
    protected function determineStoragePageIdForNewRecord(DomainObjectInterface $object = null)
928
    {
929
        $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
930
        if ($object !== null) {
931
            if (ObjectAccess::isPropertyGettable($object, 'pid')) {
932
                $pid = ObjectAccess::getProperty($object, 'pid');
933
                if (isset($pid)) {
934
                    return (int)$pid;
935
                }
936
            }
937
            $className = get_class($object);
938
            // todo: decide what to do with this option.
939
            if (isset($frameworkConfiguration['persistence']['classes'][$className]) && !empty($frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'])) {
940
                return (int)$frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'];
941
            }
942
        }
943
        $storagePidList = GeneralUtility::intExplode(',', $frameworkConfiguration['persistence']['storagePid']);
944
        return (int)$storagePidList[0];
945
    }
946
947
    /**
948
     * Returns a plain value
949
     *
950
     * i.e. objects are flattened out if possible.
951
     * Checks explicitly for null values as DataMapper's getPlainValue would convert this to 'NULL'
952
     *
953
     * @param mixed $input The value that will be converted
954
     * @param ColumnMap|null $columnMap Optional column map for retrieving the date storage format
955
     * @return int|string|null
956
     */
957
    protected function getPlainValue($input, ColumnMap $columnMap = null)
958
    {
959
        return $input !== null
960
            ? GeneralUtility::makeInstance(DataMapper::class)->getPlainValue($input, $columnMap)
961
            : null;
962
    }
963
}
964