* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\PropertyAccess\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassSetValue; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassTypeErrorInsideCall; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestSingularAndPluralProps; use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object; use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted; class PropertyAccessorTest extends TestCase { /** * @var PropertyAccessor */ private $propertyAccessor; protected function setUp(): void { $this->propertyAccessor = new PropertyAccessor(); } public function getPathsWithUnexpectedType() { return [ ['', 'foobar'], ['foo', 'foobar'], [null, 'foobar'], [123, 'foobar'], [(object) ['prop' => null], 'prop.foobar'], [(object) ['prop' => (object) ['subProp' => null]], 'prop.subProp.foobar'], [['index' => null], '[index][foobar]'], [['index' => ['subIndex' => null]], '[index][subIndex][foobar]'], ]; } public function getPathsWithMissingProperty() { return [ [(object) ['firstName' => 'Bernhard'], 'lastName'], [(object) ['property' => (object) ['firstName' => 'Bernhard']], 'property.lastName'], [['index' => (object) ['firstName' => 'Bernhard']], '[index].lastName'], [new TestClass('Bernhard'), 'protectedProperty'], [new TestClass('Bernhard'), 'privateProperty'], [new TestClass('Bernhard'), 'protectedAccessor'], [new TestClass('Bernhard'), 'protectedIsAccessor'], [new TestClass('Bernhard'), 'protectedHasAccessor'], [new TestClass('Bernhard'), 'privateAccessor'], [new TestClass('Bernhard'), 'privateIsAccessor'], [new TestClass('Bernhard'), 'privateHasAccessor'], // Properties are not camelized [new TestClass('Bernhard'), 'public_property'], ]; } public function getPathsWithMissingIndex() { return [ [['firstName' => 'Bernhard'], '[lastName]'], [[], '[index][lastName]'], [['index' => []], '[index][lastName]'], [['index' => ['firstName' => 'Bernhard']], '[index][lastName]'], [(object) ['property' => ['firstName' => 'Bernhard']], 'property[lastName]'], ]; } /** * @dataProvider getValidPropertyPaths */ public function testGetValue($objectOrArray, $path, $value) { $this->assertSame($value, $this->propertyAccessor->getValue($objectOrArray, $path)); } /** * @dataProvider getPathsWithMissingProperty */ public function testGetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path) { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException'); $this->propertyAccessor->getValue($objectOrArray, $path); } /** * @dataProvider getPathsWithMissingProperty */ public function testGetValueReturnsNullIfPropertyNotFoundAndExceptionIsDisabled($objectOrArray, $path) { $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()->disableExceptionOnInvalidPropertyPath()->getPropertyAccessor(); $this->assertNull($this->propertyAccessor->getValue($objectOrArray, $path), $path); } /** * @dataProvider getPathsWithMissingIndex */ public function testGetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path) { $this->assertNull($this->propertyAccessor->getValue($objectOrArray, $path)); } /** * @dataProvider getPathsWithMissingIndex */ public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchIndexException'); $this->propertyAccessor = new PropertyAccessor(false, true); $this->propertyAccessor->getValue($objectOrArray, $path); } public function testGetValueThrowsExceptionIfNotArrayAccess() { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchIndexException'); $this->propertyAccessor->getValue(new \stdClass(), '[index]'); } public function testGetValueReadsMagicGet() { $this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'magicProperty')); } public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath() { $object = new \ArrayObject(); $array = ['child' => ['index' => $object]]; $this->assertNull($this->propertyAccessor->getValue($array, '[child][index][foo][bar]')); $this->assertSame([], $object->getArrayCopy()); } // https://github.com/symfony/symfony/pull/4450 public function testGetValueReadsMagicGetThatReturnsConstant() { $this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'constantMagicProperty')); } public function testGetValueNotModifyObject() { $object = new \stdClass(); $object->firstName = ['Bernhard']; $this->assertNull($this->propertyAccessor->getValue($object, 'firstName[1]')); $this->assertSame(['Bernhard'], $object->firstName); } public function testGetValueNotModifyObjectException() { $propertyAccessor = new PropertyAccessor(false, true); $object = new \stdClass(); $object->firstName = ['Bernhard']; try { $propertyAccessor->getValue($object, 'firstName[1]'); } catch (NoSuchIndexException $e) { } $this->assertSame(['Bernhard'], $object->firstName); } public function testGetValueDoesNotReadMagicCallByDefault() { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException'); $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty'); } public function testGetValueReadsMagicCallIfEnabled() { $this->propertyAccessor = new PropertyAccessor(true); $this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); } // https://github.com/symfony/symfony/pull/4450 public function testGetValueReadsMagicCallThatReturnsConstant() { $this->propertyAccessor = new PropertyAccessor(true); $this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'constantMagicCallProperty')); } /** * @dataProvider getPathsWithUnexpectedType */ public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $path) { $this->expectException('Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException'); $this->expectExceptionMessage('PropertyAccessor requires a graph of objects or arrays to operate on'); $this->propertyAccessor->getValue($objectOrArray, $path); } /** * @dataProvider getValidPropertyPaths */ public function testSetValue($objectOrArray, $path) { $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); } /** * @dataProvider getPathsWithMissingProperty */ public function testSetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path) { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException'); $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); } /** * @dataProvider getPathsWithMissingIndex */ public function testSetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path) { $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); } /** * @dataProvider getPathsWithMissingIndex */ public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) { $this->propertyAccessor = new PropertyAccessor(false, true); $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); } public function testSetValueThrowsExceptionIfNotArrayAccess() { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchIndexException'); $object = new \stdClass(); $this->propertyAccessor->setValue($object, '[index]', 'Updated'); } public function testSetValueUpdatesMagicSet() { $author = new TestClassMagicGet('Bernhard'); $this->propertyAccessor->setValue($author, 'magicProperty', 'Updated'); $this->assertEquals('Updated', $author->__get('magicProperty')); } public function testSetValueThrowsExceptionIfThereAreMissingParameters() { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException'); $object = new TestClass('Bernhard'); $this->propertyAccessor->setValue($object, 'publicAccessorWithMoreRequiredParameters', 'Updated'); } public function testSetValueDoesNotUpdateMagicCallByDefault() { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException'); $author = new TestClassMagicCall('Bernhard'); $this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated'); } public function testSetValueUpdatesMagicCallIfEnabled() { $this->propertyAccessor = new PropertyAccessor(true); $author = new TestClassMagicCall('Bernhard'); $this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated'); $this->assertEquals('Updated', $author->__call('getMagicCallProperty', [])); } /** * @dataProvider getPathsWithUnexpectedType */ public function testSetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $path) { $this->expectException('Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException'); $this->expectExceptionMessage('PropertyAccessor requires a graph of objects or arrays to operate on'); $this->propertyAccessor->setValue($objectOrArray, $path, 'value'); } public function testGetValueWhenArrayValueIsNull() { $this->propertyAccessor = new PropertyAccessor(false, true); $this->assertNull($this->propertyAccessor->getValue(['index' => ['nullable' => null]], '[index][nullable]')); } /** * @dataProvider getValidPropertyPaths */ public function testIsReadable($objectOrArray, $path) { $this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path)); } /** * @dataProvider getPathsWithMissingProperty */ public function testIsReadableReturnsFalseIfPropertyNotFound($objectOrArray, $path) { $this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path)); } /** * @dataProvider getPathsWithMissingIndex */ public function testIsReadableReturnsTrueIfIndexNotFound($objectOrArray, $path) { // Non-existing indices can be read. In this case, null is returned $this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path)); } /** * @dataProvider getPathsWithMissingIndex */ public function testIsReadableReturnsFalseIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) { $this->propertyAccessor = new PropertyAccessor(false, true); // When exceptions are enabled, non-existing indices cannot be read $this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path)); } public function testIsReadableRecognizesMagicGet() { $this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicGet('Bernhard'), 'magicProperty')); } public function testIsReadableDoesNotRecognizeMagicCallByDefault() { $this->assertFalse($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); } public function testIsReadableRecognizesMagicCallIfEnabled() { $this->propertyAccessor = new PropertyAccessor(true); $this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); } /** * @dataProvider getPathsWithUnexpectedType */ public function testIsReadableReturnsFalseIfNotObjectOrArray($objectOrArray, $path) { $this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path)); } /** * @dataProvider getValidPropertyPaths */ public function testIsWritable($objectOrArray, $path) { $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path)); } /** * @dataProvider getPathsWithMissingProperty */ public function testIsWritableReturnsFalseIfPropertyNotFound($objectOrArray, $path) { $this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path)); } /** * @dataProvider getPathsWithMissingIndex */ public function testIsWritableReturnsTrueIfIndexNotFound($objectOrArray, $path) { // Non-existing indices can be written. Arrays are created on-demand. $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path)); } /** * @dataProvider getPathsWithMissingIndex */ public function testIsWritableReturnsTrueIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) { $this->propertyAccessor = new PropertyAccessor(false, true); // Non-existing indices can be written even if exceptions are enabled $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path)); } public function testIsWritableRecognizesMagicSet() { $this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicGet('Bernhard'), 'magicProperty')); } public function testIsWritableDoesNotRecognizeMagicCallByDefault() { $this->assertFalse($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); } public function testIsWritableRecognizesMagicCallIfEnabled() { $this->propertyAccessor = new PropertyAccessor(true); $this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); } /** * @dataProvider getPathsWithUnexpectedType */ public function testIsWritableReturnsFalseIfNotObjectOrArray($objectOrArray, $path) { $this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path)); } public function getValidPropertyPaths() { return [ [['Bernhard', 'Schussek'], '[0]', 'Bernhard'], [['Bernhard', 'Schussek'], '[1]', 'Schussek'], [['firstName' => 'Bernhard'], '[firstName]', 'Bernhard'], [['index' => ['firstName' => 'Bernhard']], '[index][firstName]', 'Bernhard'], [(object) ['firstName' => 'Bernhard'], 'firstName', 'Bernhard'], [(object) ['property' => ['firstName' => 'Bernhard']], 'property[firstName]', 'Bernhard'], [['index' => (object) ['firstName' => 'Bernhard']], '[index].firstName', 'Bernhard'], [(object) ['property' => (object) ['firstName' => 'Bernhard']], 'property.firstName', 'Bernhard'], // Accessor methods [new TestClass('Bernhard'), 'publicProperty', 'Bernhard'], [new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'], [new TestClass('Bernhard'), 'publicAccessorWithDefaultValue', 'Bernhard'], [new TestClass('Bernhard'), 'publicAccessorWithRequiredAndDefaultValue', 'Bernhard'], [new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'], [new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'], [new TestClass('Bernhard'), 'publicGetSetter', 'Bernhard'], [new TestClass('Bernhard'), 'publicCanAccessor', 'Bernhard'], // Methods are camelized [new TestClass('Bernhard'), 'public_accessor', 'Bernhard'], [new TestClass('Bernhard'), '_public_accessor', 'Bernhard'], // Missing indices [['index' => []], '[index][firstName]', null], [['root' => ['index' => []]], '[root][index][firstName]', null], // Special chars [['%!@$§.' => 'Bernhard'], '[%!@$§.]', 'Bernhard'], [['index' => ['%!@$§.' => 'Bernhard']], '[index][%!@$§.]', 'Bernhard'], [(object) ['%!@$§' => 'Bernhard'], '%!@$§', 'Bernhard'], [(object) ['property' => (object) ['%!@$§' => 'Bernhard']], 'property.%!@$§', 'Bernhard'], // nested objects and arrays [['foo' => new TestClass('bar')], '[foo].publicGetSetter', 'bar'], [new TestClass(['foo' => 'bar']), 'publicGetSetter[foo]', 'bar'], [new TestClass(new TestClass('bar')), 'publicGetter.publicGetSetter', 'bar'], [new TestClass(['foo' => new TestClass('bar')]), 'publicGetter[foo].publicGetSetter', 'bar'], [new TestClass(new TestClass(new TestClass('bar'))), 'publicGetter.publicGetter.publicGetSetter', 'bar'], [new TestClass(['foo' => ['baz' => new TestClass('bar')]]), 'publicGetter[foo][baz].publicGetSetter', 'bar'], ]; } public function testTicket5755() { $object = new Ticket5775Object(); $this->propertyAccessor->setValue($object, 'property', 'foobar'); $this->assertEquals('foobar', $object->getProperty()); } public function testSetValueDeepWithMagicGetter() { $obj = new TestClassMagicGet('foo'); $obj->publicProperty = ['foo' => ['bar' => 'some_value']]; $this->propertyAccessor->setValue($obj, 'publicProperty[foo][bar]', 'Updated'); $this->assertSame('Updated', $obj->publicProperty['foo']['bar']); } public function getReferenceChainObjectsForSetValue() { return [ [['a' => ['b' => ['c' => 'old-value']]], '[a][b][c]', 'new-value'], [new TestClassSetValue(new TestClassSetValue('old-value')), 'value.value', 'new-value'], [new TestClassSetValue(['a' => ['b' => ['c' => new TestClassSetValue('old-value')]]]), 'value[a][b][c].value', 'new-value'], [new TestClassSetValue(['a' => ['b' => 'old-value']]), 'value[a][b]', 'new-value'], [new \ArrayIterator(['a' => ['b' => ['c' => 'old-value']]]), '[a][b][c]', 'new-value'], ]; } /** * @dataProvider getReferenceChainObjectsForSetValue */ public function testSetValueForReferenceChainIssue($object, $path, $value) { $this->propertyAccessor->setValue($object, $path, $value); $this->assertEquals($value, $this->propertyAccessor->getValue($object, $path)); } public function getReferenceChainObjectsForIsWritable() { return [ [new TestClassIsWritable(['a' => ['b' => 'old-value']]), 'value[a][b]', false], [new TestClassIsWritable(new \ArrayIterator(['a' => ['b' => 'old-value']])), 'value[a][b]', true], [new TestClassIsWritable(['a' => ['b' => ['c' => new TestClassSetValue('old-value')]]]), 'value[a][b][c].value', true], ]; } /** * @dataProvider getReferenceChainObjectsForIsWritable */ public function testIsWritableForReferenceChainIssue($object, $path, $value) { $this->assertEquals($value, $this->propertyAccessor->isWritable($object, $path)); } public function testThrowTypeError() { $this->expectException('Symfony\Component\PropertyAccess\Exception\InvalidArgumentException'); $this->expectExceptionMessage('Expected argument of type "DateTime", "string" given at property path "date"'); $object = new TypeHinted(); $this->propertyAccessor->setValue($object, 'date', 'This is a string, \DateTime expected.'); } public function testThrowTypeErrorWithNullArgument() { $this->expectException('Symfony\Component\PropertyAccess\Exception\InvalidArgumentException'); $this->expectExceptionMessage('Expected argument of type "DateTime", "NULL" given'); $object = new TypeHinted(); $this->propertyAccessor->setValue($object, 'date', null); } public function testSetTypeHint() { $date = new \DateTime(); $object = new TypeHinted(); $this->propertyAccessor->setValue($object, 'date', $date); $this->assertSame($date, $object->getDate()); } public function testArrayNotBeeingOverwritten() { $value = ['value1' => 'foo', 'value2' => 'bar']; $object = new TestClass($value); $this->propertyAccessor->setValue($object, 'publicAccessor[value2]', 'baz'); $this->assertSame('baz', $this->propertyAccessor->getValue($object, 'publicAccessor[value2]')); $this->assertSame(['value1' => 'foo', 'value2' => 'baz'], $object->getPublicAccessor()); } public function testCacheReadAccess() { $obj = new TestClass('foo'); $propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter()); $this->assertEquals('foo', $propertyAccessor->getValue($obj, 'publicGetSetter')); $propertyAccessor->setValue($obj, 'publicGetSetter', 'bar'); $propertyAccessor->setValue($obj, 'publicGetSetter', 'baz'); $this->assertEquals('baz', $propertyAccessor->getValue($obj, 'publicGetSetter')); } public function testAttributeWithSpecialChars() { $obj = new \stdClass(); $obj->{'@foo'} = 'bar'; $obj->{'a/b'} = '1'; $obj->{'a%2Fb'} = '2'; $propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter()); $this->assertSame('bar', $propertyAccessor->getValue($obj, '@foo')); $this->assertSame('1', $propertyAccessor->getValue($obj, 'a/b')); $this->assertSame('2', $propertyAccessor->getValue($obj, 'a%2Fb')); } public function testThrowTypeErrorWithInterface() { $this->expectException('Symfony\Component\PropertyAccess\Exception\InvalidArgumentException'); $this->expectExceptionMessage('Expected argument of type "Countable", "string" given'); $object = new TypeHinted(); $this->propertyAccessor->setValue($object, 'countable', 'This is a string, \Countable expected.'); } public function testAnonymousClassRead() { $value = 'bar'; $obj = $this->generateAnonymousClass($value); $propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter()); $this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo')); } public function testAnonymousClassReadThrowExceptionOnInvalidPropertyPath() { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException'); $obj = $this->generateAnonymousClass('bar'); $this->propertyAccessor->getValue($obj, 'invalid_property'); } public function testAnonymousClassReadReturnsNullOnInvalidPropertyWithDisabledException() { $obj = $this->generateAnonymousClass('bar'); $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()->disableExceptionOnInvalidPropertyPath()->getPropertyAccessor(); $this->assertNull($this->propertyAccessor->getValue($obj, 'invalid_property')); } public function testAnonymousClassWrite() { $value = 'bar'; $obj = $this->generateAnonymousClass(''); $propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter()); $propertyAccessor->setValue($obj, 'foo', $value); $this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo')); } private function generateAnonymousClass($value) { $obj = eval('return new class($value) { private $foo; public function __construct($foo) { $this->foo = $foo; } /** * @return mixed */ public function getFoo() { return $this->foo; } /** * @param mixed $foo */ public function setFoo($foo) { $this->foo = $foo; } };'); return $obj; } public function testThrowTypeErrorInsideSetterCall() { $this->expectException('TypeError'); $object = new TestClassTypeErrorInsideCall(); $this->propertyAccessor->setValue($object, 'property', 'foo'); } public function testDoNotDiscardReturnTypeError() { $this->expectException('TypeError'); $object = new ReturnTyped(); $this->propertyAccessor->setValue($object, 'foos', [new \DateTime()]); } public function testDoNotDiscardReturnTypeErrorWhenWriterMethodIsMisconfigured() { $this->expectException('TypeError'); $object = new ReturnTyped(); $this->propertyAccessor->setValue($object, 'name', 'foo'); } public function testWriteToSingularPropertyWhilePluralOneExists() { $object = new TestSingularAndPluralProps(); $this->propertyAccessor->isWritable($object, 'email'); //cache access info $this->propertyAccessor->setValue($object, 'email', 'test@email.com'); self::assertEquals('test@email.com', $object->getEmail()); self::assertEmpty($object->getEmails()); } public function testWriteToPluralPropertyWhileSingularOneExists() { $object = new TestSingularAndPluralProps(); $this->propertyAccessor->isWritable($object, 'emails'); //cache access info $this->propertyAccessor->setValue($object, 'emails', ['test@email.com']); $this->assertEquals(['test@email.com'], $object->getEmails()); $this->assertNull($object->getEmail()); } public function testAdderAndRemoverArePreferredOverSetter() { $object = new TestPluralAdderRemoverAndSetter(); $this->propertyAccessor->isWritable($object, 'emails'); //cache access info $this->propertyAccessor->setValue($object, 'emails', ['test@email.com']); $this->assertEquals(['test@email.com'], $object->getEmails()); } public function testAdderAndRemoverArePreferredOverSetterForSameSingularAndPlural() { $object = new TestPluralAdderRemoverAndSetterSameSingularAndPlural(); $this->propertyAccessor->isWritable($object, 'aircraft'); //cache access info $this->propertyAccessor->setValue($object, 'aircraft', ['aeroplane']); $this->assertEquals(['aeroplane'], $object->getAircraft()); } }