41f907d4c18bad040014544a2df333e40360c5c4
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Generic / Backend.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Persistence\Generic;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2010-2013 Extbase Team (http://forge.typo3.org/projects/typo3v4-mvc)
8 * Extbase is a backport of TYPO3 Flow. All credits go to the TYPO3 Flow team.
9 * All rights reserved
10 *
11 * This script is part of the TYPO3 project. The TYPO3 project is
12 * free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
16 *
17 * The GNU General Public License can be found at
18 * http://www.gnu.org/copyleft/gpl.html.
19 * A copy is found in the textfile GPL.txt and important notices to the license
20 * from the author is found in LICENSE.txt distributed with these scripts.
21 *
22 *
23 * This script is distributed in the hope that it will be useful,
24 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 * GNU General Public License for more details.
27 *
28 * This copyright notice MUST APPEAR in all copies of the script!
29 ***************************************************************/
30 /**
31 * A persistence backend. This backend maps objects to the relational model of the storage backend.
32 * It persists all added, removed and changed objects.
33 */
34 class Backend implements \TYPO3\CMS\Extbase\Persistence\Generic\BackendInterface, \TYPO3\CMS\Core\SingletonInterface {
35
36 /**
37 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Session
38 */
39 protected $session;
40
41 /**
42 * @var \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface
43 */
44 protected $persistenceManager;
45
46 /**
47 * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
48 */
49 protected $aggregateRootObjects;
50
51 /**
52 * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
53 */
54 protected $deletedEntities;
55
56 /**
57 * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
58 */
59 protected $changedEntities;
60
61 /**
62 * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
63 */
64 protected $visitedDuringPersistence;
65
66 /**
67 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
68 */
69 protected $reflectionService;
70
71 /**
72 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory
73 */
74 protected $qomFactory;
75
76 /**
77 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface
78 */
79 protected $storageBackend;
80
81 /**
82 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
83 */
84 protected $dataMapper;
85
86 /**
87 * The TYPO3 reference index object
88 *
89 * @var \TYPO3\CMS\Core\Database\ReferenceIndex
90 */
91 protected $referenceIndex;
92
93 /**
94 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
95 */
96 protected $configurationManager;
97
98 /**
99 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
100 */
101 protected $signalSlotDispatcher;
102
103 /**
104 * Constructs the backend
105 *
106 * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
107 * @return void
108 */
109 public function __construct(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager) {
110 $this->configurationManager = $configurationManager;
111 $this->referenceIndex = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Database\\ReferenceIndex');
112 $this->aggregateRootObjects = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
113 $this->deletedEntities = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
114 $this->changedEntities = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
115 }
116
117 /**
118 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Session $session
119 * @return void
120 */
121 public function injectSession(\TYPO3\CMS\Extbase\Persistence\Generic\Session $session) {
122 $this->session = $session;
123 }
124
125 /**
126 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface $storageBackend
127 * @return void
128 */
129 public function injectStorageBackend(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface $storageBackend) {
130 $this->storageBackend = $storageBackend;
131 }
132
133 /**
134 * Injects the DataMapper to map nodes to objects
135 *
136 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper
137 * @return void
138 */
139 public function injectDataMapper(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper) {
140 $this->dataMapper = $dataMapper;
141 }
142
143 /**
144 * Injects the Reflection Service
145 *
146 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
147 * @return void
148 */
149 public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService) {
150 $this->reflectionService = $reflectionService;
151 }
152
153 /**
154 * Injects the QueryObjectModelFactory
155 *
156 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory $qomFactory
157 * @return void
158 */
159 public function injectQomFactory(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory $qomFactory) {
160 $this->qomFactory = $qomFactory;
161 }
162
163 /**
164 * @param \TYPO3\CMS\Extbase\SignalSlot\Dispatcher $signalSlotDispatcher
165 */
166 public function injectSignalSlotDispatcher(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher $signalSlotDispatcher) {
167 $this->signalSlotDispatcher = $signalSlotDispatcher;
168 }
169
170 /**
171 * @param \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager
172 */
173 public function setPersistenceManager(\TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager) {
174 $this->persistenceManager = $persistenceManager;
175 }
176
177 /**
178 * Returns the repository session
179 *
180 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Session
181 */
182 public function getSession() {
183 return $this->session;
184 }
185
186 /**
187 * Returns the Data Mapper
188 *
189 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
190 */
191 public function getDataMapper() {
192 return $this->dataMapper;
193 }
194
195 /**
196 * Returns the current QOM factory
197 *
198 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory
199 */
200 public function getQomFactory() {
201 return $this->qomFactory;
202 }
203
204 /**
205 * Returns the reflection service
206 *
207 * @return \TYPO3\CMS\Extbase\Reflection\ReflectionService
208 */
209 public function getReflectionService() {
210 return $this->reflectionService;
211 }
212
213 /**
214 * Returns the number of records matching the query.
215 *
216 * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
217 * @return integer
218 * @api
219 */
220 public function getObjectCountByQuery(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query) {
221 return $this->storageBackend->getObjectCountByQuery($query);
222 }
223
224 /**
225 * Returns the object data matching the $query.
226 *
227 * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
228 * @return array
229 * @api
230 */
231 public function getObjectDataByQuery(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query) {
232 return $this->storageBackend->getObjectDataByQuery($query);
233 }
234
235 /**
236 * Returns the (internal) identifier for the object, if it is known to the
237 * backend. Otherwise NULL is returned.
238 *
239 * @param object $object
240 * @return string The identifier for the object if it is known, or NULL
241 */
242 public function getIdentifierByObject($object) {
243 if ($object instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
244 $object = $object->_loadRealInstance();
245 if (!is_object($object)) {
246 return NULL;
247 }
248 }
249 if ($this->session->hasObject($object)) {
250 return $this->session->getIdentifierByObject($object);
251 } else {
252 return NULL;
253 }
254 }
255
256 /**
257 * Returns the object with the (internal) identifier, if it is known to the
258 * backend. Otherwise NULL is returned.
259 *
260 * @param string $identifier
261 * @param string $className
262 * @return object The object for the identifier if it is known, or NULL
263 */
264 public function getObjectByIdentifier($identifier, $className) {
265 if ($this->session->hasIdentifier($identifier, $className)) {
266 return $this->session->getObjectByIdentifier($identifier, $className);
267 } else {
268 $query = $this->persistenceManager->createQueryForType($className);
269 $query->getQuerySettings()->setRespectStoragePage(FALSE);
270 $query->getQuerySettings()->setRespectSysLanguage(FALSE);
271 return $query->matching($query->equals('uid', $identifier))->execute()->getFirst();
272 }
273 }
274
275 /**
276 * Checks if the given object has ever been persisted.
277 *
278 * @param object $object The object to check
279 * @return boolean TRUE if the object is new, FALSE if the object exists in the repository
280 */
281 public function isNewObject($object) {
282 return $this->getIdentifierByObject($object) === NULL;
283 }
284
285 /**
286 * Replaces the given object by the second object.
287 *
288 * This method will unregister the existing object at the identity map and
289 * register the new object instead. The existing object must therefore
290 * already be registered at the identity map which is the case for all
291 * reconstituted objects.
292 *
293 * The new object will be identified by the uid which formerly belonged
294 * to the existing object. The existing object looses its uid.
295 *
296 * @param object $existingObject The existing object
297 * @param object $newObject The new object
298 * @throws \TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException
299 * @return void
300 * @deprecated since 6.1, will be removed two versions later
301 */
302 public function replaceObject($existingObject, $newObject) {
303 $this->session->replaceReconstitutedEntity($existingObject, $newObject);
304 }
305
306 /**
307 * Sets the aggregate root objects
308 *
309 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objects
310 * @return void
311 */
312 public function setAggregateRootObjects(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $objects) {
313 $this->aggregateRootObjects = $objects;
314 }
315
316 /**
317 * Sets the changed objects
318 *
319 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $entities
320 * @return void
321 */
322 public function setChangedEntities(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $entities) {
323 $this->changedEntities = $entities;
324 }
325
326 /**
327 * Sets the deleted objects
328 *
329 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $entities
330 * @return void
331 */
332 public function setDeletedEntities(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $entities) {
333 $this->deletedEntities = $entities;
334 }
335
336 /**
337 * Commits the current persistence session.
338 *
339 * @return void
340 */
341 public function commit() {
342 $this->persistObjects();
343 $this->processDeletedObjects();
344 }
345
346 /**
347 * Sets the deleted objects
348 *
349 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objects
350 * @return void
351 * @deprecated since 6.1, will be removed two versions later
352 */
353 public function setDeletedObjects(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $objects) {
354 $this->setDeletedEntities($objects);
355 }
356
357 /**
358 * Traverse and persist all aggregate roots and their object graph.
359 *
360 * @return void
361 */
362 protected function persistObjects() {
363 $this->visitedDuringPersistence = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
364 foreach ($this->aggregateRootObjects as $object) {
365 if ($object->_isNew()) {
366 $this->insertObject($object);
367 }
368 $this->persistObject($object, NULL);
369 }
370 foreach ($this->changedEntities as $object) {
371 $this->persistObject($object, NULL);
372 }
373 }
374
375 /**
376 * Persists the given object.
377 *
378 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object to be inserted
379 * @return void
380 */
381 protected function persistObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object) {
382 if (isset($this->visitedDuringPersistence[$object])) {
383 return;
384 }
385 $row = array();
386 $queue = array();
387 $dataMap = $this->dataMapper->getDataMap(get_class($object));
388 $properties = $object->_getProperties();
389 foreach ($properties as $propertyName => $propertyValue) {
390 if (!$dataMap->isPersistableProperty($propertyName) || $this->propertyValueIsLazyLoaded($propertyValue)) {
391 continue;
392 }
393 $columnMap = $dataMap->getColumnMap($propertyName);
394 if ($propertyValue instanceof \TYPO3\CMS\Extbase\Persistence\ObjectStorage) {
395 $cleanProperty = $object->_getCleanProperty($propertyName);
396 // objectstorage needs to be persisted if the object is new, the objectstorge is dirty, meaning it has
397 // been changed after initial build, or a empty objectstorge is present and the cleanstate objectstorage
398 // has childelements, meaning all elements should been removed from the objectstorage
399 if ($object->_isNew() || $propertyValue->_isDirty() || ($propertyValue->count() == 0 && $cleanProperty && $cleanProperty->count() > 0)) {
400 $this->persistObjectStorage($propertyValue, $object, $propertyName, $row);
401 $propertyValue->_memorizeCleanState();
402 }
403 foreach ($propertyValue as $containedObject) {
404 if ($containedObject instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
405 $queue[] = $containedObject;
406 }
407 }
408 } elseif ($propertyValue instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
409 if ($object->_isDirty($propertyName)) {
410 if ($propertyValue->_isNew()) {
411 $this->insertObject($propertyValue);
412 }
413 $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue);
414 }
415 $queue[] = $propertyValue;
416 } elseif ($object->_isNew() || $object->_isDirty($propertyName)) {
417 $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue, $columnMap);
418 }
419 }
420 if (count($row) > 0) {
421 $this->updateObject($object, $row);
422 $object->_memorizeCleanState();
423 }
424 $this->visitedDuringPersistence[$object] = $object->getUid();
425 foreach ($queue as $queuedObject) {
426 $this->persistObject($queuedObject);
427 }
428 }
429
430 /**
431 * Checks, if the property value is lazy loaded and was not initialized
432 *
433 * @param mixed $propertyValue The property value
434 * @return bool
435 */
436 protected function propertyValueIsLazyLoaded($propertyValue) {
437 if ($propertyValue instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
438 return TRUE;
439 }
440 if ($propertyValue instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage) {
441 if ($propertyValue->isInitialized() === FALSE) {
442 return TRUE;
443 }
444 }
445 return FALSE;
446 }
447
448 /**
449 * Persists a an object storage. Objects of a 1:n or m:n relation are queued and processed with the parent object. A 1:1 relation
450 * gets persisted immediately. Objects which were removed from the property were detached from the parent object. They will not be
451 * deleted by default. You have to annotate the property with "@cascade remove" if you want them to be deleted as well.
452 *
453 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objectStorage The object storage to be persisted.
454 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object. One of the properties holds the object storage.
455 * @param string $propertyName The name of the property holding the object storage.
456 * @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.
457 * @return void
458 */
459 protected function persistObjectStorage(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $objectStorage, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $propertyName, array &$row) {
460 $className = get_class($parentObject);
461 $columnMap = $this->dataMapper->getDataMap($className)->getColumnMap($propertyName);
462 $propertyMetaData = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
463 foreach ($this->getRemovedChildObjects($parentObject, $propertyName) as $removedObject) {
464 $this->detachObjectFromParentObject($removedObject, $parentObject, $propertyName);
465 if ($columnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY && $propertyMetaData['cascade'] === 'remove') {
466 $this->removeEntity($removedObject);
467 }
468 }
469
470 $currentUids = array();
471 $sortingPosition = 1;
472 $updateSortingOfFollowing = FALSE;
473
474 foreach ($objectStorage as $object) {
475 if (empty($currentUids)) {
476 $sortingPosition = 1;
477 } else {
478 $sortingPosition++;
479 }
480 $cleanProperty = $parentObject->_getCleanProperty($propertyName);
481 if ($object->_isNew()) {
482 $this->insertObject($object);
483 $this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
484 // if a new object is inserted, all objects after this need to have their sorting updated
485 $updateSortingOfFollowing = TRUE;
486 } elseif ($cleanProperty === NULL || $cleanProperty->getPosition($object) === NULL) {
487 // if parent object is new then it doesn't have cleanProperty yet; before attaching object it's clean position is null
488 $this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
489 // 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
490 $updateSortingOfFollowing = TRUE;
491 } elseif ($objectStorage->isRelationDirty($object) || $cleanProperty->getPosition($object) !== $objectStorage->getPosition($object)) {
492 $this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
493 $updateSortingOfFollowing = TRUE;
494 } elseif ($updateSortingOfFollowing) {
495 if ($sortingPosition > $objectStorage->getPosition($object)) {
496 $this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
497 } else {
498 $sortingPosition = $objectStorage->getPosition($object);
499 }
500 }
501 $currentUids[] = $object->getUid();
502 }
503
504 if ($columnMap->getParentKeyFieldName() === NULL) {
505 $row[$columnMap->getColumnName()] = implode(',', $currentUids);
506 } else {
507 $row[$columnMap->getColumnName()] = $this->dataMapper->countRelated($parentObject, $propertyName);
508 }
509 }
510
511 /**
512 * Returns the removed objects determined by a comparison of the clean property value
513 * with the actual property value.
514 *
515 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object
516 * @param string $propertyName
517 * @return array An array of removed objects
518 */
519 protected function getRemovedChildObjects(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, $propertyName) {
520 $removedObjects = array();
521 $cleanPropertyValue = $object->_getCleanProperty($propertyName);
522 if (is_array($cleanPropertyValue) || $cleanPropertyValue instanceof \Iterator) {
523 $propertyValue = $object->_getProperty($propertyName);
524 foreach ($cleanPropertyValue as $containedObject) {
525 if (!$propertyValue->contains($containedObject)) {
526 $removedObjects[] = $containedObject;
527 }
528 }
529 }
530 return $removedObjects;
531 }
532
533 /**
534 * Updates the fields defining the relation between the object and the parent object.
535 *
536 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
537 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject
538 * @param string $parentPropertyName
539 * @param integer $sortingPosition
540 * @return void
541 */
542 protected function attachObjectToParentObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $parentPropertyName, $sortingPosition = 0) {
543 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
544 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
545 if ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
546 $this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition);
547 } elseif ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
548 $this->insertRelationInRelationtable($object, $parentObject, $parentPropertyName, $sortingPosition);
549 }
550 }
551
552 /**
553 * Updates the fields defining the relation between the object and the parent object.
554 *
555 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
556 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject
557 * @param string $parentPropertyName
558 * @param integer $sortingPosition
559 * @return void
560 */
561 protected function updateRelationOfObjectToParentObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $parentObject, $parentPropertyName, $sortingPosition = 0) {
562 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
563 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
564 if ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
565 $this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition);
566 } elseif ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
567 $this->updateRelationInRelationTable($object, $parentObject, $parentPropertyName, $sortingPosition);
568 }
569 }
570
571 /**
572 * Updates fields defining the relation between the object and the parent object in relation has-many.
573 *
574 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
575 * @param \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $parentObject
576 * @param string $parentPropertyName
577 * @param integer $sortingPosition
578 * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException
579 * @return void
580 */
581 protected function attachObjectToParentObjectRelationHasMany(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $parentObject, $parentPropertyName, $sortingPosition = 0) {
582 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
583 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
584 if ($parentColumnMap->getTypeOfRelation() !== \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
585 throw new \TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException(
586 'Parent column relation type is ' . $parentColumnMap->getTypeOfRelation() .
587 ' but should be ' . \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY,
588 1345368105
589 );
590 }
591 $row = array();
592 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
593 if ($parentKeyFieldName !== NULL) {
594 $row[$parentKeyFieldName] = $parentObject->getUid();
595 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
596 if ($parentTableFieldName !== NULL) {
597 $row[$parentTableFieldName] = $parentDataMap->getTableName();
598 }
599 }
600 $childSortByFieldName = $parentColumnMap->getChildSortByFieldName();
601 if (!empty($childSortByFieldName)) {
602 $row[$childSortByFieldName] = $sortingPosition;
603 }
604 if (!empty($row)) {
605 $this->updateObject($object, $row);
606 }
607 }
608
609 /**
610 * Updates the fields defining the relation between the object and the parent object.
611 *
612 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
613 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject
614 * @param string $parentPropertyName
615 * @return void
616 */
617 protected function detachObjectFromParentObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $parentPropertyName) {
618 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
619 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
620 if ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
621 $row = array();
622 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
623 if ($parentKeyFieldName !== NULL) {
624 $row[$parentKeyFieldName] = '';
625 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
626 if ($parentTableFieldName !== NULL) {
627 $row[$parentTableFieldName] = '';
628 }
629 }
630 $childSortByFieldName = $parentColumnMap->getChildSortByFieldName();
631 if (!empty($childSortByFieldName)) {
632 $row[$childSortByFieldName] = 0;
633 }
634 if (count($row) > 0) {
635 $this->updateObject($object, $row);
636 }
637 } elseif ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
638 $this->deleteRelationFromRelationtable($object, $parentObject, $parentPropertyName);
639 }
640 }
641
642 /**
643 * Inserts an object in the storage backend
644 *
645 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object to be insterted in the storage
646 * @return void
647 */
648 protected function insertObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object) {
649 if ($object instanceof \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject) {
650 $result = $this->getUidOfAlreadyPersistedValueObject($object);
651 if ($result !== FALSE) {
652 $object->_setProperty('uid', (integer) $result);
653 return;
654 }
655 }
656 $dataMap = $this->dataMapper->getDataMap(get_class($object));
657 $row = array();
658 $this->addCommonFieldsToRow($object, $row);
659 if ($dataMap->getLanguageIdColumnName() !== NULL) {
660 $row[$dataMap->getLanguageIdColumnName()] = -1;
661 }
662 $uid = $this->storageBackend->addRow($dataMap->getTableName(), $row);
663 $object->_setProperty('uid', (integer) $uid);
664 if ((integer) $uid >= 1) {
665 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterInsertObject', array('object' => $object));
666 }
667 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
668 if ($frameworkConfiguration['persistence']['updateReferenceIndex'] === '1') {
669 $this->referenceIndex->updateRefIndexTable($dataMap->getTableName(), $uid);
670 }
671 $this->session->registerObject($object, $uid);
672 }
673
674 /**
675 * Tests, if the given Value Object already exists in the storage backend and if so, it returns the uid.
676 *
677 * @param \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object The object to be tested
678 * @return mixed The matching uid if an object was found, else FALSE
679 */
680 protected function getUidOfAlreadyPersistedValueObject(\TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object) {
681 return $this->storageBackend->getUidOfAlreadyPersistedValueObject($object);
682 }
683
684 /**
685 * Inserts mm-relation into a relation table
686 *
687 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The related object
688 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object
689 * @param string $propertyName The name of the parent object's property where the related objects are stored in
690 * @param integer $sortingPosition Defaults to NULL
691 * @return int The uid of the inserted row
692 */
693 protected function insertRelationInRelationtable(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $propertyName, $sortingPosition = NULL) {
694 $dataMap = $this->dataMapper->getDataMap(get_class($parentObject));
695 $columnMap = $dataMap->getColumnMap($propertyName);
696 $row = array(
697 $columnMap->getParentKeyFieldName() => (integer) $parentObject->getUid(),
698 $columnMap->getChildKeyFieldName() => (integer) $object->getUid(),
699 $columnMap->getChildSortByFieldName() => !is_null($sortingPosition) ? (integer) $sortingPosition : 0
700 );
701 $relationTableName = $columnMap->getRelationTableName();
702 // FIXME Reenable support for tablenames
703 // $childTableName = $columnMap->getChildTableName();
704 // if (isset($childTableName)) {
705 // $row['tablenames'] = $childTableName;
706 // }
707 if ($columnMap->getRelationTablePageIdColumnName() !== NULL) {
708 $row[$columnMap->getRelationTablePageIdColumnName()] = $this->determineStoragePageIdForNewRecord();
709 }
710 $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
711 if (count($relationTableMatchFields)) {
712 foreach ($relationTableMatchFields as $matchField => $matchValue) {
713 $row[$matchField] = $matchValue;
714 }
715 }
716 $relationTableInsertFields = $columnMap->getRelationTableInsertFields();
717 if (count($relationTableInsertFields)) {
718 foreach ($relationTableInsertFields as $insertField => $insertValue) {
719 $row[$insertField] = $insertValue;
720 }
721 }
722 $res = $this->storageBackend->addRow($relationTableName, $row, TRUE);
723 return $res;
724 }
725
726 /**
727 * Inserts mm-relation into a relation table
728 *
729 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The related object
730 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object
731 * @param string $propertyName The name of the parent object's property where the related objects are stored in
732 * @param integer $sortingPosition Defaults to NULL
733 * @return integer The uid of the inserted row
734 */
735 protected function updateRelationInRelationTable(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $propertyName, $sortingPosition = 0) {
736 $dataMap = $this->dataMapper->getDataMap(get_class($parentObject));
737 $columnMap = $dataMap->getColumnMap($propertyName);
738 $row = array(
739 $columnMap->getParentKeyFieldName() => (int)$parentObject->getUid(),
740 $columnMap->getChildKeyFieldName() => (int)$object->getUid(),
741 $columnMap->getChildSortByFieldName() => (int)$sortingPosition
742 );
743 $relationTableName = $columnMap->getRelationTableName();
744 // FIXME Reenable support for tablenames
745 // $childTableName = $columnMap->getChildTableName();
746 // if (isset($childTableName)) {
747 // $row['tablenames'] = $childTableName;
748 // }
749
750 $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
751 if (is_array($relationTableMatchFields) && count($relationTableMatchFields) > 0) {
752 $row = array_merge($relationTableMatchFields, $row);
753 }
754 $res = $this->storageBackend->updateRelationTableRow(
755 $relationTableName,
756 $row);
757 return $res;
758 }
759
760 /**
761 * Delete all mm-relations of a parent from a relation table
762 *
763 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object
764 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
765 * @return bool
766 */
767 protected function deleteAllRelationsFromRelationtable(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $parentPropertyName) {
768 $dataMap = $this->dataMapper->getDataMap(get_class($parentObject));
769 $columnMap = $dataMap->getColumnMap($parentPropertyName);
770 $relationTableName = $columnMap->getRelationTableName();
771 $relationMatchFields = array(
772 $columnMap->getParentKeyFieldName() => (integer) $parentObject->getUid()
773 );
774 $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
775 if (is_array($relationTableMatchFields) && count($relationTableMatchFields) > 0) {
776 $relationMatchFields = array_merge($relationTableMatchFields, $relationMatchFields);
777 }
778 $res = $this->storageBackend->removeRow($relationTableName, $relationMatchFields, FALSE);
779 return $res;
780 }
781
782 /**
783 * Delete an mm-relation from a relation table
784 *
785 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $relatedObject The related object
786 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object
787 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
788 * @return bool
789 */
790 protected function deleteRelationFromRelationtable(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $relatedObject, \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $parentPropertyName) {
791 $dataMap = $this->dataMapper->getDataMap(get_class($parentObject));
792 $columnMap = $dataMap->getColumnMap($parentPropertyName);
793 $relationTableName = $columnMap->getRelationTableName();
794 $res = $this->storageBackend->removeRow($relationTableName, array(
795 $columnMap->getParentKeyFieldName() => (integer) $parentObject->getUid(),
796 $columnMap->getChildKeyFieldName() => (integer) $relatedObject->getUid()
797 ), FALSE);
798 return $res;
799 }
800
801 /**
802 * Fetches maximal value currently used for sorting field in parent table
803 *
804 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject The parent object
805 * @param string $parentPropertyName The name of the parent object's property where the related objects are stored in
806 * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException
807 * @return mixed the max value
808 */
809 protected function fetchMaxSortingFromParentTable(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $parentObject, $parentPropertyName) {
810 $parentDataMap = $this->dataMapper->getDataMap(get_class($parentObject));
811 $parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
812 if ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
813 $tableName = $parentColumnMap->getChildTableName();
814 $sortByFieldName = $parentColumnMap->getChildSortByFieldName();
815
816 if (empty($sortByFieldName)) {
817 return FALSE;
818 }
819 $matchFields = array();
820 $parentKeyFieldName = $parentColumnMap->getParentKeyFieldName();
821 if ($parentKeyFieldName !== NULL) {
822 $matchFields[$parentKeyFieldName] = $parentObject->getUid();
823 $parentTableFieldName = $parentColumnMap->getParentTableFieldName();
824 if ($parentTableFieldName !== NULL) {
825 $matchFields[$parentTableFieldName] = $parentDataMap->getTableName();
826 }
827 }
828
829 if (empty($matchFields)) {
830 return FALSE;
831 }
832 } elseif ($parentColumnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
833 $tableName = $parentColumnMap->getRelationTableName();
834 $sortByFieldName = $parentColumnMap->getChildSortByFieldName();
835
836 $matchFields = array(
837 $parentColumnMap->getParentKeyFieldName() => (int)$parentObject->getUid()
838 );
839
840 $relationTableMatchFields = $parentColumnMap->getRelationTableMatchFields();
841 if (is_array($relationTableMatchFields) && count($relationTableMatchFields) > 0) {
842 $matchFields = array_merge($relationTableMatchFields, $matchFields);
843 }
844 } else {
845 throw new \TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException('Unexpected parent column relation type:' . $parentColumnMap->getTypeOfRelation(), 1345368106);
846 }
847
848 $result = $this->storageBackend->getMaxValueFromTable(
849 $tableName,
850 $matchFields,
851 $sortByFieldName);
852 return $result;
853 }
854
855 /**
856 * Updates a given object in the storage
857 *
858 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object to be updated
859 * @param array $row Row to be stored
860 * @return bool
861 */
862 protected function updateObject(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, array $row) {
863 $dataMap = $this->dataMapper->getDataMap(get_class($object));
864 $this->addCommonFieldsToRow($object, $row);
865 $row['uid'] = $object->getUid();
866 if ($dataMap->getLanguageIdColumnName() !== NULL) {
867 $row[$dataMap->getLanguageIdColumnName()] = $object->_getProperty('_languageUid');
868 if ($object->_getProperty('_localizedUid') !== NULL) {
869 $row['uid'] = $object->_getProperty('_localizedUid');
870 }
871 }
872 $res = $this->storageBackend->updateRow($dataMap->getTableName(), $row);
873 if ($res === TRUE) {
874 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterUpdateObject', array('object' => $object));
875 }
876 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
877 if ($frameworkConfiguration['persistence']['updateReferenceIndex'] === '1') {
878 $this->referenceIndex->updateRefIndexTable($dataMap->getTableName(), $row['uid']);
879 }
880 return $res;
881 }
882
883 /**
884 * Adds common databse fields to a row
885 *
886 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
887 * @param array &$row
888 * @return void
889 */
890 protected function addCommonFieldsToRow(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, array &$row) {
891 $dataMap = $this->dataMapper->getDataMap(get_class($object));
892 $this->addCommonDateFieldsToRow($object, $row);
893 if ($dataMap->getRecordTypeColumnName() !== NULL && $dataMap->getRecordType() !== NULL) {
894 $row[$dataMap->getRecordTypeColumnName()] = $dataMap->getRecordType();
895 }
896 if ($object->_isNew() && !isset($row['pid'])) {
897 $row['pid'] = $this->determineStoragePageIdForNewRecord($object);
898 }
899 }
900
901 /**
902 * Adjustes the common date fields of the given row to the current time
903 *
904 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
905 * @param array &$row The row to be updated
906 * @return void
907 */
908 protected function addCommonDateFieldsToRow(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, array &$row) {
909 $dataMap = $this->dataMapper->getDataMap(get_class($object));
910 if ($object->_isNew() && $dataMap->getCreationDateColumnName() !== NULL) {
911 $row[$dataMap->getCreationDateColumnName()] = $GLOBALS['EXEC_TIME'];
912 }
913 if ($dataMap->getModificationDateColumnName() !== NULL) {
914 $row[$dataMap->getModificationDateColumnName()] = $GLOBALS['EXEC_TIME'];
915 }
916 }
917
918 /**
919 * Iterate over deleted aggregate root objects and process them
920 *
921 * @return void
922 */
923 protected function processDeletedObjects() {
924 foreach ($this->deletedEntities as $entity) {
925 if ($this->session->hasObject($entity)) {
926 $this->removeEntity($entity);
927 $this->session->unregisterReconstitutedEntity($entity);
928 $this->session->unregisterObject($entity);
929 }
930 }
931 $this->deletedEntities = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
932 }
933
934 /**
935 * Deletes an object
936 *
937 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object to be removed from the storage
938 * @param boolean $markAsDeleted Wether to just flag the row deleted (default) or really delete it
939 * @return void
940 */
941 protected function removeEntity(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object, $markAsDeleted = TRUE) {
942 $dataMap = $this->dataMapper->getDataMap(get_class($object));
943 $tableName = $dataMap->getTableName();
944 if ($markAsDeleted === TRUE && $dataMap->getDeletedFlagColumnName() !== NULL) {
945 $deletedColumnName = $dataMap->getDeletedFlagColumnName();
946 $row = array(
947 'uid' => $object->getUid(),
948 $deletedColumnName => 1
949 );
950 $this->addCommonDateFieldsToRow($object, $row);
951 $res = $this->storageBackend->updateRow($tableName, $row);
952 } else {
953 $res = $this->storageBackend->removeRow($tableName, array('uid' => $object->getUid()));
954 }
955 if ($res === TRUE) {
956 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterRemoveObject', array('object' => $object));
957 }
958 $this->removeRelatedObjects($object);
959 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
960 if ($frameworkConfiguration['persistence']['updateReferenceIndex'] === '1') {
961 $this->referenceIndex->updateRefIndexTable($tableName, $object->getUid());
962 }
963 }
964
965 /**
966 * Remove related objects
967 *
968 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object The object to scanned for related objects
969 * @return void
970 */
971 protected function removeRelatedObjects(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object) {
972 $className = get_class($object);
973 $dataMap = $this->dataMapper->getDataMap($className);
974 $classSchema = $this->reflectionService->getClassSchema($className);
975 $properties = $object->_getProperties();
976 foreach ($properties as $propertyName => $propertyValue) {
977 $columnMap = $dataMap->getColumnMap($propertyName);
978 $propertyMetaData = $classSchema->getProperty($propertyName);
979 if ($propertyMetaData['cascade'] === 'remove') {
980 if ($columnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
981 foreach ($propertyValue as $containedObject) {
982 $this->removeEntity($containedObject);
983 }
984 } elseif ($propertyValue instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
985 $this->removeEntity($propertyValue);
986 }
987 }
988 }
989 }
990
991 /**
992 * Determine the storage page ID for a given NEW record
993 *
994 * This does the following:
995 * - If the domain object has an accessible property 'pid' (i.e. through a getPid() method), that is used to store the record.
996 * - If there is a TypoScript configuration "classes.CLASSNAME.newRecordStoragePid", that is used to store new records.
997 * - If there is no such TypoScript configuration, it uses the first value of The "storagePid" taken for reading records.
998 *
999 * @param \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object
1000 * @return int the storage Page ID where the object should be stored
1001 */
1002 protected function determineStoragePageIdForNewRecord(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface $object = NULL) {
1003 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
1004 if ($object !== NULL) {
1005 if (\TYPO3\CMS\Extbase\Reflection\ObjectAccess::isPropertyGettable($object, 'pid')) {
1006 $pid = \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getProperty($object, 'pid');
1007 if (isset($pid)) {
1008 return (integer) $pid;
1009 }
1010 }
1011 $className = get_class($object);
1012 if (isset($frameworkConfiguration['persistence']['classes'][$className]) && !empty($frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'])) {
1013 return (integer) $frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'];
1014 }
1015 }
1016 $storagePidList = \TYPO3\CMS\Core\Utility\GeneralUtility::intExplode(',', $frameworkConfiguration['persistence']['storagePid']);
1017 return (integer) $storagePidList[0];
1018 }
1019
1020 /**
1021 * Returns a plain value, i.e. objects are flattened out if possible.
1022 *
1023 * @param mixed $input
1024 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap $columnMap
1025 * @return mixed
1026 */
1027 protected function getPlainValue($input, $columnMap = NULL) {
1028 if ($input instanceof \DateTime) {
1029 if (!is_null($columnMap) && !is_null($columnMap->getDateTimeStorageFormat())) {
1030 if ($columnMap->getDateTimeStorageFormat() == 'datetime') {
1031 return $input->format('Y-m-d H:i:s');
1032 }
1033 if ($columnMap->getDateTimeStorageFormat() == 'date') {
1034 return $input->format('Y-m-d');
1035 }
1036 } else {
1037 return $input->format('U');
1038 }
1039 } elseif ($input instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
1040 return $input->getUid();
1041 } elseif (is_bool($input)) {
1042 return $input === TRUE ? 1 : 0;
1043 } else {
1044 return $input;
1045 }
1046 }
1047 }
1048
1049 ?>