6464742e9d3bbe7e8550a2789c29890a34c5c0eb
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Tests / Unit / Persistence / Generic / Mapper / DataMapperTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Extbase\Tests\Unit\Persistence\Generic\Mapper;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
19 use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException;
20 use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
21 use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
22 use TYPO3\CMS\Extbase\Reflection\ClassSchema;
23 use TYPO3\TestingFramework\Core\AccessibleObjectInterface;
24 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
25
26 /**
27 * Test case
28 */
29 class DataMapperTest extends UnitTestCase
30 {
31 /**
32 * This test does not actually test anything rather than map calls both mocked methods getTargetType and mapSingleRow
33 * while completely ignoring the result of the method.
34 * @todo: Cover this functionality by a functional test
35 *
36 * @test
37 */
38 public function mapMapsArrayToObjectByCallingmapToObject()
39 {
40 $rows = [['uid' => '1234']];
41 $object = new \stdClass();
42
43 /** @var DataMapper|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $dataMapper */
44 $dataMapper = $this->getMockBuilder(DataMapper::class)
45 ->setConstructorArgs([
46 $this->createMock(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class),
47 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory::class),
48 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Session::class),
49 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory::class),
50 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface::class),
51 $this->createMock(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface::class),
52 $this->createMock(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class),
53 ])
54 ->setMethods(['mapSingleRow', 'getTargetType'])
55 ->getMock();
56
57 $dataMapper->expects($this->any())->method('getTargetType')->will($this->returnArgument(1));
58 $dataMapper->expects($this->once())->method('mapSingleRow')->with($rows[0])->will($this->returnValue($object));
59
60 $dataMapper->map(get_class($object), $rows);
61 }
62
63 /**
64 * This test does not actually test anything rather than mapSingleRow delegates functionality to
65 * the persistence session which is a mock itself.
66 * @todo: Cover this functionality by a functional test
67 *
68 * @test
69 */
70 public function mapSingleRowReturnsObjectFromPersistenceSessionIfAvailable()
71 {
72 $row = ['uid' => '1234'];
73 $object = new \stdClass();
74 $persistenceSession = $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Session::class);
75 $persistenceSession->expects($this->once())->method('hasIdentifier')->with('1234')->will($this->returnValue(true));
76 $persistenceSession->expects($this->once())->method('getObjectByIdentifier')->with('1234')->will($this->returnValue($object));
77
78 $dataMapper = $this->getAccessibleMock(
79 DataMapper::class,
80 ['dummy'],
81 [
82 $this->createMock(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class),
83 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory::class),
84 $persistenceSession,
85 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory::class),
86 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface::class),
87 $this->createMock(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface::class),
88 $this->createMock(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class),
89 ]
90 );
91
92 $dataMapper->_call('mapSingleRow', get_class($object), $row);
93 }
94
95 /**
96 * This test has a far too complex setup to test a single unit. This actually is a functional test, accomplished
97 * by mocking the whole dependency chain. This test only tests code structure while it should test functionality.
98 * @todo: Cover this functionality by a functional test
99 *
100 * @test
101 */
102 public function thawPropertiesSetsPropertyValues()
103 {
104 $className = Fixture\DummyEntity::class;
105 $object = new Fixture\DummyEntity();
106 $row = [
107 'uid' => '1234',
108 'firstProperty' => 'firstValue',
109 'secondProperty' => 1234,
110 'thirdProperty' => 1.234,
111 'fourthProperty' => false
112 ];
113 $columnMaps = [
114 'uid' => new ColumnMap('uid', 'uid'),
115 'pid' => new ColumnMap('pid', 'pid'),
116 'firstProperty' => new ColumnMap('firstProperty', 'firstProperty'),
117 'secondProperty' => new ColumnMap('secondProperty', 'secondProperty'),
118 'thirdProperty' => new ColumnMap('thirdProperty', 'thirdProperty')
119 ];
120 $dataMap = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap::class, ['dummy'], [$className, $className]);
121 $dataMap->_set('columnMaps', $columnMaps);
122 $dataMaps = [
123 $className => $dataMap
124 ];
125 /** @var AccessibleObjectInterface|\TYPO3\CMS\Extbase\Reflection\ClassSchema $classSchema */
126 $classSchema = new ClassSchema($className);
127 $mockReflectionService = $this->getMockBuilder(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class)
128 ->setMethods(['getClassSchema'])
129 ->getMock();
130 $mockReflectionService->expects($this->any())->method('getClassSchema')->will($this->returnValue($classSchema));
131 $dataMapFactory = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory::class, ['dummy'], [], '', false);
132 $dataMapFactory->_set('dataMaps', $dataMaps);
133 $dataMapper = $this->getAccessibleMock(
134 DataMapper::class,
135 ['dummy'],
136 [
137 $mockReflectionService,
138 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory::class),
139 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Session::class),
140 $dataMapFactory,
141 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface::class),
142 $this->createMock(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface::class),
143 $this->createMock(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class),
144 ]
145 );
146 $dataMapper->_call('thawProperties', $object, $row);
147
148 $this->assertAttributeEquals('firstValue', 'firstProperty', $object);
149 $this->assertAttributeEquals(1234, 'secondProperty', $object);
150 $this->assertAttributeEquals(1.234, 'thirdProperty', $object);
151 $this->assertAttributeEquals(false, 'fourthProperty', $object);
152 }
153
154 /**
155 * Test if fetchRelatedEager method returns NULL when $fieldValue = '' and relation type == RELATION_HAS_ONE
156 *
157 * This is a actually a functional test as it tests multiple units along with a very specific setup of dependencies.
158 * @todo: Cover this functionality by a functional test
159 *
160 * @test
161 */
162 public function fetchRelatedEagerReturnsNullForEmptyRelationHasOne()
163 {
164 $columnMap = new ColumnMap('columnName', 'propertyName');
165 $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_ONE);
166 $dataMap = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap::class)
167 ->setMethods(['getColumnMap'])
168 ->disableOriginalConstructor()
169 ->getMock();
170 $dataMap->expects($this->any())->method('getColumnMap')->will($this->returnValue($columnMap));
171 $dataMapper = $this->getAccessibleMock(DataMapper::class, ['getDataMap'], [], '', false);
172 $dataMapper->expects($this->any())->method('getDataMap')->will($this->returnValue($dataMap));
173 $result = $dataMapper->_call('fetchRelatedEager', $this->createMock(\TYPO3\CMS\Extbase\DomainObject\AbstractEntity::class), 'SomeName', '');
174 $this->assertEquals(null, $result);
175 }
176
177 /**
178 * Test if fetchRelatedEager method returns empty array when $fieldValue = '' and relation type != RELATION_HAS_ONE
179 *
180 * This is a actually a functional test as it tests multiple units along with a very specific setup of dependencies.
181 * @todo: Cover this functionality by a functional test
182 *
183 * @test
184 */
185 public function fetchRelatedEagerReturnsEmptyArrayForEmptyRelationNotHasOne()
186 {
187 $columnMap = new ColumnMap('columnName', 'propertyName');
188 $columnMap->setTypeOfRelation(ColumnMap::RELATION_BELONGS_TO_MANY);
189 $dataMap = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap::class)
190 ->setMethods(['getColumnMap'])
191 ->disableOriginalConstructor()
192 ->getMock();
193 $dataMap->expects($this->any())->method('getColumnMap')->will($this->returnValue($columnMap));
194 $dataMapper = $this->getAccessibleMock(DataMapper::class, ['getDataMap'], [], '', false);
195 $dataMapper->expects($this->any())->method('getDataMap')->will($this->returnValue($dataMap));
196 $result = $dataMapper->_call('fetchRelatedEager', $this->createMock(\TYPO3\CMS\Extbase\DomainObject\AbstractEntity::class), 'SomeName', '');
197 $this->assertEquals([], $result);
198 }
199
200 /**
201 * Test if fetchRelatedEager method returns NULL when $fieldValue = ''
202 * and relation type == RELATION_HAS_ONE without calling fetchRelated
203 *
204 * This is a actually a functional test as it tests multiple units along with a very specific setup of dependencies.
205 * @todo: Cover this functionality by a functional test
206 *
207 * @test
208 */
209 public function MapObjectToClassPropertyReturnsNullForEmptyRelationHasOne()
210 {
211 $columnMap = new ColumnMap('columnName', 'propertyName');
212 $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_ONE);
213 $dataMap = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap::class)
214 ->setMethods(['getColumnMap'])
215 ->disableOriginalConstructor()
216 ->getMock();
217 $dataMap->expects($this->any())->method('getColumnMap')->will($this->returnValue($columnMap));
218 $dataMapper = $this->getAccessibleMock(DataMapper::class, ['getDataMap', 'fetchRelated'], [], '', false);
219 $dataMapper->expects($this->any())->method('getDataMap')->will($this->returnValue($dataMap));
220 $dataMapper->expects($this->never())->method('fetchRelated');
221 $result = $dataMapper->_call('mapObjectToClassProperty', $this->createMock(\TYPO3\CMS\Extbase\DomainObject\AbstractEntity::class), 'SomeName', '');
222 $this->assertEquals(null, $result);
223 }
224
225 /**
226 * Test if mapObjectToClassProperty method returns objects
227 * that are already registered in the persistence session
228 * without query it from the persistence layer
229 *
230 * This is a actually a functional test as it tests multiple units along with a very specific setup of dependencies.
231 * @todo: Cover this functionality by a functional test
232 *
233 * @test
234 */
235 public function mapObjectToClassPropertyReturnsExistingObjectWithoutCallingFetchRelated()
236 {
237 $columnMap = new ColumnMap('columnName', 'propertyName');
238 $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_ONE);
239 $dataMap = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap::class)
240 ->setMethods(['getColumnMap'])
241 ->disableOriginalConstructor()
242 ->getMock();
243
244 $object = new Fixture\DummyParentEntity();
245 $child = new Fixture\DummyChildEntity();
246
247 /** @var \TYPO3\CMS\Extbase\Reflection\ClassSchema|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $classSchema1 */
248 $classSchema1 = new ClassSchema(Fixture\DummyParentEntity::class);
249 $identifier = 1;
250
251 $session = new \TYPO3\CMS\Extbase\Persistence\Generic\Session();
252 $session->registerObject($child, $identifier);
253
254 $mockReflectionService = $this->getMockBuilder(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class)
255 ->setMethods(['getClassSchema'])
256 ->disableOriginalConstructor()
257 ->getMock();
258 $mockReflectionService->expects($this->any())->method('getClassSchema')->will($this->returnValue($classSchema1));
259
260 $dataMap->expects($this->any())->method('getColumnMap')->will($this->returnValue($columnMap));
261
262 $dataMapper = $this->getAccessibleMock(
263 DataMapper::class,
264 ['getDataMap', 'getNonEmptyRelationValue'],
265 [
266 $mockReflectionService,
267 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory::class),
268 $session,
269 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory::class),
270 $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface::class),
271 $this->createMock(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface::class),
272 $this->createMock(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class),
273 ]
274 );
275 $dataMapper->expects($this->any())->method('getDataMap')->will($this->returnValue($dataMap));
276 $dataMapper->expects($this->never())->method('getNonEmptyRelationValue');
277 $result = $dataMapper->_call('mapObjectToClassProperty', $object, 'relationProperty', $identifier);
278 $this->assertEquals($child, $result);
279 }
280
281 /**
282 * Data provider for date checks. Date will be stored based on UTC in
283 * the database. That's why it's not possible to check for explicit date
284 * strings but using the date('c') conversion instead, which considers the
285 * current local timezone setting.
286 *
287 * @return array
288 */
289 public function mapDateTimeHandlesDifferentFieldEvaluationsDataProvider()
290 {
291 return [
292 'nothing' => [null, null, null],
293 'timestamp' => [1, null, date('c', 1)],
294 'empty date' => ['0000-00-00', 'date', null],
295 'valid date' => ['2013-01-01', 'date', date('c', strtotime('2013-01-01T00:00:00+00:00'))],
296 'empty datetime' => ['0000-00-00 00:00:00', 'datetime', null],
297 'valid datetime' => ['2013-01-01 01:02:03', 'datetime', date('c', strtotime('2013-01-01T01:02:03+00:00'))],
298 ];
299 }
300
301 /**
302 * @param string|int|null $value
303 * @param string|null $storageFormat
304 * @param string|null $expectedValue
305 * @test
306 * @dataProvider mapDateTimeHandlesDifferentFieldEvaluationsDataProvider
307 */
308 public function mapDateTimeHandlesDifferentFieldEvaluations($value, $storageFormat, $expectedValue)
309 {
310 /** @var DataMapper|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $accessibleDataMapFactory */
311 $accessibleDataMapFactory = $this->getAccessibleMock(DataMapper::class, ['dummy'], [], '', false);
312
313 /** @var $dateTime NULL|\DateTime */
314 $dateTime = $accessibleDataMapFactory->_callRef('mapDateTime', $value, $storageFormat);
315
316 if ($expectedValue === null) {
317 $this->assertNull($dateTime);
318 } else {
319 $this->assertEquals($expectedValue, $dateTime->format('c'));
320 }
321 }
322
323 /**
324 * @test
325 */
326 public function testMapDateTimeHandlesSubclassesOfDateTime()
327 {
328 /** @var DataMapper|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $accessibleDataMapFactory */
329 $accessibleDataMapFactory = $this->getAccessibleMock(DataMapper::class, ['dummy'], [], '', false);
330 $targetType = 'TYPO3\CMS\Extbase\Tests\Unit\Persistence\Fixture\Model\CustomDateTime';
331 $date = '2013-01-01 01:02:03';
332 $storageFormat = 'datetime';
333
334 /** @var $dateTime NULL|\DateTime */
335 $dateTime = $accessibleDataMapFactory->_callRef('mapDateTime', $date, $storageFormat, $targetType);
336
337 $this->assertInstanceOf($targetType, $dateTime);
338 }
339
340 /**
341 * @test
342 */
343 public function getPlainValueReturnsCorrectDateTimeFormat()
344 {
345 /** @var DataMapper $subject */
346 $subject = $this->createPartialMock(DataMapper::class, ['dummy']);
347
348 $columnMap = new ColumnMap('column_name', 'propertyName');
349 $columnMap->setDateTimeStorageFormat('datetime');
350 $datetimeAsString = '2013-04-15 09:30:00';
351 $input = new \DateTime($datetimeAsString, new \DateTimeZone('UTC'));
352 $this->assertEquals('2013-04-15 09:30:00', $subject->getPlainValue($input, $columnMap));
353 $columnMap->setDateTimeStorageFormat('date');
354 $this->assertEquals('2013-04-15', $subject->getPlainValue($input, $columnMap));
355 }
356
357 /**
358 * @test
359 * @dataProvider getPlainValueReturnsExpectedValuesDataProvider
360 */
361 public function getPlainValueReturnsExpectedValues($expectedValue, $input)
362 {
363 /** @var DataMapper $dataMapper */
364 $dataMapper = $this->createPartialMock(DataMapper::class, ['dummy']);
365
366 $this->assertSame($expectedValue, $dataMapper->getPlainValue($input));
367 }
368
369 /**
370 * @return array
371 */
372 public function getPlainValueReturnsExpectedValuesDataProvider()
373 {
374 $traversableDomainObject = $this->prophesize()
375 ->willImplement(\Iterator::class)
376 ->willImplement(DomainObjectInterface::class);
377 $traversableDomainObject->getUid()->willReturn(1);
378
379 return [
380 'datetime to timestamp' => ['1365866253', new \DateTime('@1365866253')],
381 'boolean true to 1' => [1, true],
382 'boolean false to 0' => [0, false],
383 'NULL is handled as string' => ['NULL', null],
384 'string value is returned unchanged' => ['RANDOM string', 'RANDOM string'],
385 'array is flattened' => ['a,b,c', ['a', 'b', 'c']],
386 'deep array is flattened' => ['a,b,c', [['a', 'b'], 'c']],
387 'traversable domain object to identifier' => [1, $traversableDomainObject->reveal()],
388 'integer value is returned unchanged' => [1234, 1234],
389 'float is converted to string' => ['1234.56', 1234.56],
390 ];
391 }
392
393 /**
394 * @test
395 */
396 public function getPlainValueCallsGetRealInstanceOnInputIfInputIsInstanceOfLazyLoadingProxy()
397 {
398 $this->expectException(UnexpectedTypeException::class);
399 $this->expectExceptionCode(1274799934);
400
401 /** @var DataMapper $dataMapper */
402 $dataMapper = $this->createPartialMock(DataMapper::class, ['dummy']);
403 $input = $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy::class);
404 $input->expects($this->once())->method('_loadRealInstance')->will($this->returnValue($dataMapper));
405 $dataMapper->getPlainValue($input);
406 }
407
408 /**
409 * @test
410 */
411 public function getPlainValueCallsGetUidOnDomainObjectInterfaceInput()
412 {
413 /** @var DataMapper $dataMapper */
414 $dataMapper = $this->createPartialMock(DataMapper::class, ['dummy']);
415 $input = $this->createMock(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface::class);
416
417 $input->expects($this->once())->method('getUid')->will($this->returnValue(23));
418 $this->assertSame(23, $dataMapper->getPlainValue($input));
419 }
420 }