[BUGFIX] Clone ObjectStorage in ObjectAccess
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Tests / Unit / Reflection / ObjectAccessTest.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Tests\Unit\Reflection;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16 use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
17 use TYPO3\CMS\Extbase\Reflection\Exception\PropertyNotAccessibleException;
18 use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
19 use TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture\DummyClassWithGettersAndSetters;
20
21 /**
22 * Test case
23 */
24 class ObjectAccessTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
25 {
26 /**
27 * @var DummyClassWithGettersAndSetters
28 */
29 protected $dummyObject;
30
31 /**
32 * Set up
33 */
34 protected function setUp()
35 {
36 $this->dummyObject = new DummyClassWithGettersAndSetters();
37 $this->dummyObject->setProperty('string1');
38 $this->dummyObject->setAnotherProperty(42);
39 $this->dummyObject->shouldNotBePickedUp = true;
40 }
41
42 /**
43 * @test
44 */
45 public function getPropertyReturnsExpectedValueForGetterProperty()
46 {
47 $property = ObjectAccess::getProperty($this->dummyObject, 'property');
48 $this->assertEquals($property, 'string1');
49 }
50
51 /**
52 * @test
53 */
54 public function getPropertyReturnsExpectedValueForPublicProperty()
55 {
56 $property = ObjectAccess::getProperty($this->dummyObject, 'publicProperty2');
57 $this->assertEquals($property, 42, 'A property of a given object was not returned correctly.');
58 }
59
60 /**
61 * @test
62 */
63 public function getPropertyReturnsExpectedValueForUnexposedPropertyIfForceDirectAccessIsTrue()
64 {
65 $property = ObjectAccess::getProperty($this->dummyObject, 'unexposedProperty', true);
66 $this->assertEquals($property, 'unexposed', 'A property of a given object was not returned correctly.');
67 }
68
69 /**
70 * @test
71 */
72 public function getPropertyReturnsExpectedValueForUnknownPropertyIfForceDirectAccessIsTrue()
73 {
74 $this->dummyObject->unknownProperty = 'unknown';
75 $property = ObjectAccess::getProperty($this->dummyObject, 'unknownProperty', true);
76 $this->assertEquals($property, 'unknown', 'A property of a given object was not returned correctly.');
77 }
78
79 /**
80 * @test
81 */
82 public function getPropertyThrowsPropertyNotAccessibleExceptionForNotExistingPropertyIfForceDirectAccessIsTrue()
83 {
84 $this->expectException(PropertyNotAccessibleException::class);
85 $this->expectExceptionCode(1302855001);
86 ObjectAccess::getProperty($this->dummyObject, 'notExistingProperty', true);
87 }
88
89 /**
90 * @test
91 */
92 public function getPropertyThrowsExceptionIfPropertyDoesNotExist()
93 {
94 $this->expectException(PropertyNotAccessibleException::class);
95 $this->expectExceptionCode(1476109666);
96 ObjectAccess::getProperty($this->dummyObject, 'notExistingProperty');
97 }
98
99 /**
100 * @test
101 */
102 public function getPropertyReturnsNullIfArrayKeyDoesNotExist()
103 {
104 $result = ObjectAccess::getProperty([], 'notExistingProperty');
105 $this->assertNull($result);
106 }
107
108 /**
109 * @test
110 */
111 public function getPropertyTriesToCallABooleanGetterMethodIfItExists()
112 {
113 $property = ObjectAccess::getProperty($this->dummyObject, 'booleanProperty');
114 $this->assertTrue($property);
115 }
116
117 /**
118 * @test
119 */
120 public function getPropertyThrowsExceptionIfThePropertyNameIsNotAString()
121 {
122 $this->expectException(\InvalidArgumentException::class);
123 $this->expectExceptionCode(1231178303);
124 ObjectAccess::getProperty($this->dummyObject, new \ArrayObject());
125 }
126
127 /**
128 * @test
129 */
130 public function setPropertyThrowsExceptionIfThePropertyNameIsNotAString()
131 {
132 $this->expectException(\InvalidArgumentException::class);
133 $this->expectExceptionCode(1231178878);
134 ObjectAccess::setProperty($this->dummyObject, new \ArrayObject(), 42);
135 }
136
137 /**
138 * @test
139 */
140 public function setPropertyReturnsFalseIfPropertyIsNotAccessible()
141 {
142 $this->assertFalse(ObjectAccess::setProperty($this->dummyObject, 'protectedProperty', 42));
143 }
144
145 /**
146 * @test
147 */
148 public function setPropertySetsValueIfPropertyIsNotAccessibleWhenForceDirectAccessIsTrue()
149 {
150 $this->assertTrue(ObjectAccess::setProperty($this->dummyObject, 'unexposedProperty', 'was set anyway', true));
151 $this->assertAttributeEquals('was set anyway', 'unexposedProperty', $this->dummyObject);
152 }
153
154 /**
155 * @test
156 */
157 public function setPropertySetsValueIfPropertyDoesNotExistWhenForceDirectAccessIsTrue()
158 {
159 $this->assertTrue(ObjectAccess::setProperty($this->dummyObject, 'unknownProperty', 'was set anyway', true));
160 $this->assertAttributeEquals('was set anyway', 'unknownProperty', $this->dummyObject);
161 }
162
163 /**
164 * @test
165 */
166 public function setPropertyCallsASetterMethodToSetThePropertyValueIfOneIsAvailable()
167 {
168 ObjectAccess::setProperty($this->dummyObject, 'property', 4242);
169 $this->assertEquals($this->dummyObject->getProperty(), 4242, 'setProperty does not work with setter.');
170 }
171
172 /**
173 * @test
174 */
175 public function setPropertyWorksWithPublicProperty()
176 {
177 ObjectAccess::setProperty($this->dummyObject, 'publicProperty', 4242);
178 $this->assertEquals($this->dummyObject->publicProperty, 4242, 'setProperty does not work with public property.');
179 }
180
181 /**
182 * @test
183 */
184 public function setPropertyCanDirectlySetValuesInAnArrayObjectOrArray()
185 {
186 $arrayObject = new \ArrayObject();
187 $array = [];
188 ObjectAccess::setProperty($arrayObject, 'publicProperty', 4242);
189 ObjectAccess::setProperty($array, 'key', 'value');
190 $this->assertEquals(4242, $arrayObject['publicProperty']);
191 $this->assertEquals('value', $array['key']);
192 }
193
194 /**
195 * @test
196 */
197 public function getPropertyCanAccessPropertiesOfAnArrayObject()
198 {
199 $arrayObject = new \ArrayObject(['key' => 'value']);
200 $actual = ObjectAccess::getProperty($arrayObject, 'key');
201 $this->assertEquals('value', $actual, 'getProperty does not work with ArrayObject property.');
202 }
203
204 /**
205 * @test
206 */
207 public function getPropertyCanAccessPropertiesOfAnObjectStorageObject()
208 {
209 $objectStorage = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
210 $object = new \stdClass();
211 $objectStorage->attach($object);
212 $actual = ObjectAccess::getProperty($objectStorage, 0);
213 $this->assertSame($object, $actual, 'getProperty does not work with ObjectStorage property.');
214 }
215
216 /**
217 * @test
218 */
219 public function getPropertyCanAccessPropertiesOfAnObjectImplementingArrayAccess()
220 {
221 $arrayAccessInstance = new \TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture\ArrayAccessClass(['key' => 'value']);
222 $actual = ObjectAccess::getProperty($arrayAccessInstance, 'key');
223 $this->assertEquals('value', $actual, 'getProperty does not work with Array Access property.');
224 }
225
226 /**
227 * @test
228 */
229 public function getPropertyCanAccessPropertiesOfArrayAccessWithGetterMethodWhenOffsetNotExists()
230 {
231 $arrayAccessInstance = new \TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture\ArrayAccessClass([]);
232 $actual = ObjectAccess::getProperty($arrayAccessInstance, 'virtual');
233 $this->assertEquals('default-value', $actual, 'getProperty does not work with Array Access property.');
234 }
235
236 /**
237 * @test
238 */
239 public function getPropertyCanAccessPropertiesOfArrayAccessWithPriorityForOffsetIfOffsetExists()
240 {
241 $arrayAccessInstance = new \TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture\ArrayAccessClass(['virtual' => 'overridden-value']);
242 $actual = ObjectAccess::getProperty($arrayAccessInstance, 'virtual');
243 $this->assertEquals('overridden-value', $actual, 'getProperty does not work with Array Access property.');
244 }
245
246 /**
247 * @test
248 */
249 public function getPropertyCanAccessPropertiesOfAnArray()
250 {
251 $array = ['key' => 'value'];
252 $expected = ObjectAccess::getProperty($array, 'key');
253 $this->assertEquals($expected, 'value', 'getProperty does not work with Array property.');
254 }
255
256 /**
257 * @test
258 */
259 public function getPropertyPathCanAccessPropertiesOfAnArray()
260 {
261 $array = ['parent' => ['key' => 'value']];
262 $actual = ObjectAccess::getPropertyPath($array, 'parent.key');
263 $this->assertEquals('value', $actual, 'getPropertyPath does not work with Array property.');
264 }
265
266 /**
267 * @test
268 */
269 public function getPropertyPathCanAccessPropertiesOfAnObjectImplementingArrayAccess()
270 {
271 $array = ['parent' => new \ArrayObject(['key' => 'value'])];
272 $actual = ObjectAccess::getPropertyPath($array, 'parent.key');
273 $this->assertEquals('value', $actual, 'getPropertyPath does not work with Array Access property.');
274 }
275
276 /**
277 * @test
278 */
279 public function getPropertyPathCanAccessPropertiesOfAnExtbaseObjectStorageObject()
280 {
281 $objectStorage = $this->setUpObjectStorageWithTwoItems();
282 $array = [
283 'parent' => $objectStorage,
284 ];
285 $this->assertSame('value', ObjectAccess::getPropertyPath($array, 'parent.0.key'));
286 $this->assertSame('value2', ObjectAccess::getPropertyPath($array, 'parent.1.key'));
287 }
288
289 /**
290 * @test
291 */
292 public function getPropertyPathOnObjectStorageDoesNotAffectOngoingLoop()
293 {
294 $objectStorage = $this->setUpObjectStorageWithTwoItems();
295 $i = 0;
296 foreach ($objectStorage as $object) {
297 ObjectAccess::getPropertyPath($objectStorage, '0.key');
298 $i++;
299 }
300 $this->assertSame(2, $i);
301 }
302
303 /**
304 * @return ObjectStorage
305 */
306 protected function setUpObjectStorageWithTwoItems()
307 {
308 $objectStorage = new ObjectStorage();
309 $exampleObject = new \stdClass();
310 $exampleObject->key = 'value';
311 $exampleObject2 = new \stdClass();
312 $exampleObject2->key = 'value2';
313 $objectStorage->attach($exampleObject);
314 $objectStorage->attach($exampleObject2);
315 return $objectStorage;
316 }
317
318 /**
319 * @test
320 */
321 public function getPropertyPathCanAccessPropertiesOfAnSplObjectStorageObject()
322 {
323 $objectStorage = $this->setUpSplObjectStorageWithTwoItems();
324 $array = [
325 'parent' => $objectStorage,
326 ];
327 $this->assertSame('value', ObjectAccess::getPropertyPath($array, 'parent.0.key'));
328 $this->assertSame('value2', ObjectAccess::getPropertyPath($array, 'parent.1.key'));
329 }
330
331 /**
332 * @test
333 */
334 public function getPropertyPathOnSplObjectStorageDoesNotAffectOngoingLoop()
335 {
336 $objectStorage = $this->setUpSplObjectStorageWithTwoItems();
337 $i = 0;
338 foreach ($objectStorage as $object) {
339 ObjectAccess::getPropertyPath($objectStorage, '0.key');
340 $i++;
341 }
342 $this->assertSame(2, $i);
343 }
344
345 /**
346 * @return \SplObjectStorage
347 */
348 protected function setUpSplObjectStorageWithTwoItems()
349 {
350 $objectStorage = new \SplObjectStorage();
351 $exampleObject = new \stdClass();
352 $exampleObject->key = 'value';
353 $exampleObject2 = new \stdClass();
354 $exampleObject2->key = 'value2';
355 $objectStorage->attach($exampleObject);
356 $objectStorage->attach($exampleObject2);
357 return $objectStorage;
358 }
359
360 /**
361 * @test
362 */
363 public function getGettablePropertyNamesReturnsAllPropertiesWhichAreAvailable()
364 {
365 $gettablePropertyNames = ObjectAccess::getGettablePropertyNames($this->dummyObject);
366 $expectedPropertyNames = ['anotherBooleanProperty', 'anotherProperty', 'booleanProperty', 'property', 'property2', 'publicProperty', 'publicProperty2', 'someValue'];
367 $this->assertEquals($gettablePropertyNames, $expectedPropertyNames, 'getGettablePropertyNames returns not all gettable properties.');
368 }
369
370 /**
371 * @test
372 */
373 public function getGettablePropertyNamesRespectsMethodArguments()
374 {
375 $dateTimeZone = new \DateTimeZone('+2');
376 $gettablePropertyNames = ObjectAccess::getGettablePropertyNames($dateTimeZone);
377 $expectedPropertyNames = ['location', 'name'];
378 $this->assertArraySubset($expectedPropertyNames, $gettablePropertyNames);
379 }
380
381 /**
382 * @test
383 */
384 public function getSettablePropertyNamesReturnsAllPropertiesWhichAreAvailable()
385 {
386 $settablePropertyNames = ObjectAccess::getSettablePropertyNames($this->dummyObject);
387 $expectedPropertyNames = ['anotherBooleanProperty', 'anotherProperty', 'property', 'property2', 'publicProperty', 'publicProperty2', 'writeOnlyMagicProperty'];
388 $this->assertEquals($settablePropertyNames, $expectedPropertyNames, 'getSettablePropertyNames returns not all settable properties.');
389 }
390
391 /**
392 * @test
393 */
394 public function getSettablePropertyNamesReturnsPropertyNamesOfStdClass()
395 {
396 $stdClassObject = new \stdClass();
397 $stdClassObject->property = 'string1';
398 $stdClassObject->property2 = null;
399 $settablePropertyNames = ObjectAccess::getSettablePropertyNames($stdClassObject);
400 $expectedPropertyNames = ['property', 'property2'];
401 $this->assertEquals($expectedPropertyNames, $settablePropertyNames, 'getSettablePropertyNames returns not all settable properties.');
402 }
403
404 /**
405 * @test
406 */
407 public function getGettablePropertiesReturnsTheCorrectValuesForAllProperties()
408 {
409 $allProperties = ObjectAccess::getGettableProperties($this->dummyObject);
410 $expectedProperties = [
411 'anotherBooleanProperty' => true,
412 'anotherProperty' => 42,
413 'booleanProperty' => true,
414 'property' => 'string1',
415 'property2' => null,
416 'publicProperty' => null,
417 'publicProperty2' => 42,
418 'someValue' => true,
419 ];
420 $this->assertEquals($allProperties, $expectedProperties, 'expectedProperties did not return the right values for the properties.');
421 }
422
423 /**
424 * @test
425 */
426 public function getGettablePropertiesReturnsPropertiesOfStdClass()
427 {
428 $stdClassObject = new \stdClass();
429 $stdClassObject->property = 'string1';
430 $stdClassObject->property2 = null;
431 $stdClassObject->publicProperty2 = 42;
432 $allProperties = ObjectAccess::getGettableProperties($stdClassObject);
433 $expectedProperties = [
434 'property' => 'string1',
435 'property2' => null,
436 'publicProperty2' => 42
437 ];
438 $this->assertEquals($expectedProperties, $allProperties, 'expectedProperties did not return the right values for the properties.');
439 }
440
441 /**
442 * @test
443 */
444 public function isPropertySettableTellsIfAPropertyCanBeSet()
445 {
446 $this->assertTrue(ObjectAccess::isPropertySettable($this->dummyObject, 'writeOnlyMagicProperty'));
447 $this->assertTrue(ObjectAccess::isPropertySettable($this->dummyObject, 'publicProperty'));
448 $this->assertTrue(ObjectAccess::isPropertySettable($this->dummyObject, 'property'));
449 $this->assertFalse(ObjectAccess::isPropertySettable($this->dummyObject, 'privateProperty'));
450 $this->assertFalse(ObjectAccess::isPropertySettable($this->dummyObject, 'shouldNotBePickedUp'));
451 }
452
453 /**
454 * @test
455 */
456 public function isPropertySettableWorksOnStdClass()
457 {
458 $stdClassObject = new \stdClass();
459 $stdClassObject->property = 'foo';
460 $this->assertTrue(ObjectAccess::isPropertySettable($stdClassObject, 'property'));
461 $this->assertFalse(ObjectAccess::isPropertySettable($stdClassObject, 'undefinedProperty'));
462 }
463
464 /**
465 * @dataProvider propertyGettableTestValues
466 * @test
467 *
468 * @param string $property
469 * @param bool $expected
470 */
471 public function isPropertyGettableTellsIfAPropertyCanBeRetrieved($property, $expected)
472 {
473 $this->assertEquals($expected, ObjectAccess::isPropertyGettable($this->dummyObject, $property));
474 }
475
476 /**
477 * @return array
478 */
479 public function propertyGettableTestValues()
480 {
481 return [
482 ['publicProperty', true],
483 ['property', true],
484 ['booleanProperty', true],
485 ['anotherBooleanProperty', true],
486 ['privateProperty', false],
487 ['writeOnlyMagicProperty', false]
488 ];
489 }
490
491 /**
492 * @test
493 */
494 public function isPropertyGettableWorksOnArrayAccessObjects()
495 {
496 $arrayObject = new \ArrayObject();
497 $arrayObject['key'] = 'v';
498 $this->assertTrue(ObjectAccess::isPropertyGettable($arrayObject, 'key'));
499 $this->assertFalse(ObjectAccess::isPropertyGettable($arrayObject, 'undefinedKey'));
500 }
501
502 /**
503 * @test
504 */
505 public function isPropertyGettableWorksOnStdClass()
506 {
507 $stdClassObject = new \stdClass();
508 $stdClassObject->property = 'foo';
509 $this->assertTrue(ObjectAccess::isPropertyGettable($stdClassObject, 'property'));
510 $this->assertFalse(ObjectAccess::isPropertyGettable($stdClassObject, 'undefinedProperty'));
511 }
512
513 /**
514 * @test
515 */
516 public function getPropertyPathCanRecursivelyGetPropertiesOfAnObject()
517 {
518 $alternativeObject = new DummyClassWithGettersAndSetters();
519 $alternativeObject->setProperty('test');
520 $this->dummyObject->setProperty2($alternativeObject);
521 $expected = 'test';
522 $actual = ObjectAccess::getPropertyPath($this->dummyObject, 'property2.property');
523 $this->assertEquals($expected, $actual);
524 }
525
526 /**
527 * @test
528 */
529 public function getPropertyPathReturnsNullForNonExistingPropertyPath()
530 {
531 $alternativeObject = new DummyClassWithGettersAndSetters();
532 $alternativeObject->setProperty(new \stdClass());
533 $this->dummyObject->setProperty2($alternativeObject);
534 $this->assertNull(ObjectAccess::getPropertyPath($this->dummyObject, 'property2.property.not.existing'));
535 }
536
537 /**
538 * @test
539 */
540 public function getPropertyPathReturnsNullIfSubjectIsNoObject()
541 {
542 $string = 'Hello world';
543 $this->assertNull(ObjectAccess::getPropertyPath($string, 'property2'));
544 }
545
546 /**
547 * @test
548 */
549 public function getPropertyPathReturnsNullIfSubjectOnPathIsNoObject()
550 {
551 $object = new \stdClass();
552 $object->foo = 'Hello World';
553 $this->assertNull(ObjectAccess::getPropertyPath($object, 'foo.bar'));
554 }
555 }