The PropertyAccess Component

The PropertyAccess Component

The PropertyAccess component provides function to read and write from/to an object or array using a simple string notation.

Installation

  1. $ composer require symfony/property-access

Note

If you install this component outside of a Symfony application, you must require the vendor/autoload.php file in your code to enable the class autoloading mechanism provided by Composer. Read this article for more details.

Usage

The entry point of this component is the createPropertyAccessor() factory. This factory will create a new instance of the Symfony\Component\PropertyAccess\PropertyAccessor class with the default configuration:

  1. use Symfony\Component\PropertyAccess\PropertyAccess;
  2. $propertyAccessor = PropertyAccess::createPropertyAccessor();

Reading from Arrays

You can read an array with the getValue() method. This is done using the index notation that is used in PHP:

  1. // ...
  2. $person = [
  3. 'first_name' => 'Wouter',
  4. ];
  5. var_dump($propertyAccessor->getValue($person, '[first_name]')); // 'Wouter'
  6. var_dump($propertyAccessor->getValue($person, '[age]')); // null

As you can see, the method will return null if the index does not exist. But you can change this behavior with the enableExceptionOnInvalidIndex() method:

  1. // ...
  2. $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
  3. ->enableExceptionOnInvalidIndex()
  4. ->getPropertyAccessor();
  5. $person = [
  6. 'first_name' => 'Wouter',
  7. ];
  8. // instead of returning null, the code now throws an exception of type
  9. // Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
  10. $value = $propertyAccessor->getValue($person, '[age]');

You can also use multi dimensional arrays:

  1. // ...
  2. $persons = [
  3. [
  4. 'first_name' => 'Wouter',
  5. ],
  6. [
  7. 'first_name' => 'Ryan',
  8. ]
  9. ];
  10. var_dump($propertyAccessor->getValue($persons, '[0][first_name]')); // 'Wouter'
  11. var_dump($propertyAccessor->getValue($persons, '[1][first_name]')); // 'Ryan'

Reading from Objects

The `getValue() method is a very robust method, and you can see all of its features when working with objects.

Accessing public Properties

To read from properties, use the “dot” notation:

  1. // ...
  2. $person = new Person();
  3. $person->firstName = 'Wouter';
  4. var_dump($propertyAccessor->getValue($person, 'firstName')); // 'Wouter'
  5. $child = new Person();
  6. $child->firstName = 'Bar';
  7. $person->children = [$child];
  8. var_dump($propertyAccessor->getValue($person, 'children[0].firstName')); // 'Bar'

Caution

Accessing public properties is the last option used by PropertyAccessor. It tries to access the value using the below methods first before using the property directly. For example, if you have a public property that has a getter method, it will use the getter.

Using Getters

The getValue() method also supports reading using getters. The method will be created using common naming conventions for getters. It transforms the property name to camelCase (first_namebecomesFirstName) and prefixes it withget. So the actual method becomesgetFirstName():

  1. // ...
  2. class Person
  3. {
  4. private $firstName = 'Wouter';
  5. public function getFirstName()
  6. {
  7. return $this->firstName;
  8. }
  9. }
  10. $person = new Person();
  11. var_dump($propertyAccessor->getValue($person, 'first_name')); // 'Wouter'

Using Hassers/Issers

And it doesn’t even stop there. If there is no getter found, the accessor will look for an isser or hasser. This method is created using the same way as getters, this means that you can do something like this:

  1. // ...
  2. class Person
  3. {
  4. private $author = true;
  5. private $children = [];
  6. public function isAuthor()
  7. {
  8. return $this->author;
  9. }
  10. public function hasChildren()
  11. {
  12. return 0 !== count($this->children);
  13. }
  14. }
  15. $person = new Person();
  16. if ($propertyAccessor->getValue($person, 'author')) {
  17. var_dump('This person is an author');
  18. }
  19. if ($propertyAccessor->getValue($person, 'children')) {
  20. var_dump('This person has children');
  21. }

This will produce: This person is an author

Accessing a non Existing Property Path

By default a Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException is thrown if the property path passed to getValue() does not exist. You can change this behavior using the disableExceptionOnInvalidPropertyPath() method:

  1. // ...
  2. class Person
  3. {
  4. public $name;
  5. }
  6. $person = new Person();
  7. $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
  8. ->disableExceptionOnInvalidPropertyPath()
  9. ->getPropertyAccessor();
  10. // instead of throwing an exception the following code returns null
  11. $value = $propertyAccessor->getValue($person, 'birthday');

Magic `__get() Method

The getValue() method can also use the magic__get() method:

  1. // ...
  2. class Person
  3. {
  4. private $children = [
  5. 'Wouter' => [...],
  6. ];
  7. public function __get($id)
  8. {
  9. return $this->children[$id];
  10. }
  11. }
  12. $person = new Person();
  13. var_dump($propertyAccessor->getValue($person, 'Wouter')); // [...]

New in version 5.2: The magic `__get() method can be disabled since in Symfony 5.2. see Enable other Features.

Magic `__call() Method

Lastly, getValue() can use the magic__call() method, but you need to enable this feature by using Symfony\Component\PropertyAccess\PropertyAccessorBuilder:

  1. // ...
  2. class Person
  3. {
  4. private $children = [
  5. 'wouter' => [...],
  6. ];
  7. public function __call($name, $args)
  8. {
  9. $property = lcfirst(substr($name, 3));
  10. if ('get' === substr($name, 0, 3)) {
  11. return $this->children[$property] ?? null;
  12. } elseif ('set' === substr($name, 0, 3)) {
  13. $value = 1 == count($args) ? $args[0] : null;
  14. $this->children[$property] = $value;
  15. }
  16. }
  17. }
  18. $person = new Person();
  19. // enables PHP __call() magic method
  20. $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
  21. ->enableMagicCall()
  22. ->getPropertyAccessor();
  23. var_dump($propertyAccessor->getValue($person, 'wouter')); // [...]

Caution

The `__call() feature is disabled by default, you can enable it by calling enableMagicCall() see Enable other Features.

Writing to Arrays

The PropertyAccessor class can do more than just read an array, it can also write to an array. This can be achieved using the setValue() method:

  1. // ...
  2. $person = [];
  3. $propertyAccessor->setValue($person, '[first_name]', 'Wouter');
  4. var_dump($propertyAccessor->getValue($person, '[first_name]')); // 'Wouter'
  5. // or
  6. // var_dump($person['first_name']); // 'Wouter'

Writing to Objects

The setValue() method has the same features as thegetValue() method. You can use setters, the magic `__set() method or properties to set values:

  1. // ...
  2. class Person
  3. {
  4. public $firstName;
  5. private $lastName;
  6. private $children = [];
  7. public function setLastName($name)
  8. {
  9. $this->lastName = $name;
  10. }
  11. public function getLastName()
  12. {
  13. return $this->lastName;
  14. }
  15. public function getChildren()
  16. {
  17. return $this->children;
  18. }
  19. public function __set($property, $value)
  20. {
  21. $this->$property = $value;
  22. }
  23. }
  24. $person = new Person();
  25. $propertyAccessor->setValue($person, 'firstName', 'Wouter');
  26. $propertyAccessor->setValue($person, 'lastName', 'de Jong'); // setLastName is called
  27. $propertyAccessor->setValue($person, 'children', [new Person()]); // __set is called
  28. var_dump($person->firstName); // 'Wouter'
  29. var_dump($person->getLastName()); // 'de Jong'
  30. var_dump($person->getChildren()); // [Person()];

You can also use `__call() to set values but you need to enable the feature, see Enable other Features:

  1. // ...
  2. class Person
  3. {
  4. private $children = [];
  5. public function __call($name, $args)
  6. {
  7. $property = lcfirst(substr($name, 3));
  8. if ('get' === substr($name, 0, 3)) {
  9. return $this->children[$property] ?? null;
  10. } elseif ('set' === substr($name, 0, 3)) {
  11. $value = 1 == count($args) ? $args[0] : null;
  12. $this->children[$property] = $value;
  13. }
  14. }
  15. }
  16. $person = new Person();
  17. // Enable magic __call
  18. $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
  19. ->enableMagicCall()
  20. ->getPropertyAccessor();
  21. $propertyAccessor->setValue($person, 'wouter', [...]);
  22. var_dump($person->getWouter()); // [...]

New in version 5.2: The magic `__set() method can be disabled since in Symfony 5.2. see Enable other Features.

Writing to Array Properties

The PropertyAccessor class allows to update the content of arrays stored in properties through adder and remover methods:

  1. // ...
  2. class Person
  3. {
  4. /**
  5. * @var string[]
  6. */
  7. private $children = [];
  8. public function getChildren(): array
  9. {
  10. return $this->children;
  11. }
  12. public function addChild(string $name): void
  13. {
  14. $this->children[$name] = $name;
  15. }
  16. public function removeChild(string $name): void
  17. {
  18. unset($this->children[$name]);
  19. }
  20. }
  21. $person = new Person();
  22. $propertyAccessor->setValue($person, 'children', ['kevin', 'wouter']);
  23. var_dump($person->getChildren()); // ['kevin', 'wouter']

The PropertyAccess component checks for methods called add<SingularOfThePropertyName>() andremove(). Both methods must be defined. For instance, in the previous example, the component looks for the addChild() andremoveChild() methods to access to the children property. The Inflector component is used to find the singular of a property name.

If available, adder and remover methods have priority over a setter method.

Using non-standard adder/remover methods

Sometimes, adder and remover methods don’t use the standard add or remove prefix, like in this example:

  1. // ...
  2. class PeopleList
  3. {
  4. // ...
  5. public function joinPeople(string $people): void
  6. {
  7. $this->peoples[] = $people;
  8. }
  9. public function leavePeople(string $people): void
  10. {
  11. foreach ($this->peoples as $id => $item) {
  12. if ($people === $item) {
  13. unset($this->peoples[$id]);
  14. break;
  15. }
  16. }
  17. }
  18. }
  19. use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
  20. use Symfony\Component\PropertyAccess\PropertyAccessor;
  21. $list = new PeopleList();
  22. $reflectionExtractor = new ReflectionExtractor(null, null, ['join', 'leave']);
  23. $propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH, null, $reflectionExtractor, $reflectionExtractor);
  24. $propertyAccessor->setValue($person, 'peoples', ['kevin', 'wouter']);
  25. var_dump($person->getPeoples()); // ['kevin', 'wouter']

Instead of calling add<SingularOfThePropertyName>() andremove(), the PropertyAccess component will call join<SingularOfThePropertyName>() andleave() methods.

Checking Property Paths

When you want to check whether getValue() can safely be called without actually calling that method, you can use isReadable() instead:

  1. $person = new Person();
  2. if ($propertyAccessor->isReadable($person, 'firstName')) {
  3. // ...
  4. }

The same is possible for setValue(): Call the isWritable() method to find out whether a property path can be updated:

  1. $person = new Person();
  2. if ($propertyAccessor->isWritable($person, 'firstName')) {
  3. // ...
  4. }

Mixing Objects and Arrays

You can also mix objects and arrays:

  1. // ...
  2. class Person
  3. {
  4. public $firstName;
  5. private $children = [];
  6. public function setChildren($children)
  7. {
  8. $this->children = $children;
  9. }
  10. public function getChildren()
  11. {
  12. return $this->children;
  13. }
  14. }
  15. $person = new Person();
  16. $propertyAccessor->setValue($person, 'children[0]', new Person);
  17. // equal to $person->getChildren()[0] = new Person()
  18. $propertyAccessor->setValue($person, 'children[0].firstName', 'Wouter');
  19. // equal to $person->getChildren()[0]->firstName = 'Wouter'
  20. var_dump('Hello '.$propertyAccessor->getValue($person, 'children[0].firstName')); // 'Wouter'
  21. // equal to $person->getChildren()[0]->firstName

Enable other Features

The Symfony\Component\PropertyAccess\PropertyAccessor can be configured to enable extra features. To do that you could use the Symfony\Component\PropertyAccess\PropertyAccessorBuilder:

  1. // ...
  2. $propertyAccessorBuilder = PropertyAccess::createPropertyAccessorBuilder();
  3. $propertyAccessorBuilder->enableMagicCall(); // enables magic __call
  4. $propertyAccessorBuilder->enableMagicGet(); // enables magic __get
  5. $propertyAccessorBuilder->enableMagicSet(); // enables magic __set
  6. $propertyAccessorBuilder->enableMagicMethods(); // enables magic __get, __set and __call
  7. $propertyAccessorBuilder->disableMagicCall(); // disables magic __call
  8. $propertyAccessorBuilder->disableMagicGet(); // disables magic __get
  9. $propertyAccessorBuilder->disableMagicSet(); // disables magic __set
  10. $propertyAccessorBuilder->disableMagicMethods(); // disables magic __get, __set and __call
  11. // checks if magic __call, __get or __set handling are enabled
  12. $propertyAccessorBuilder->isMagicCallEnabled(); // true or false
  13. $propertyAccessorBuilder->isMagicGetEnabled(); // true or false
  14. $propertyAccessorBuilder->isMagicSetEnabled(); // true or false
  15. // At the end get the configured property accessor
  16. $propertyAccessor = $propertyAccessorBuilder->getPropertyAccessor();
  17. // Or all in one
  18. $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
  19. ->enableMagicCall()
  20. ->getPropertyAccessor();

Or you can pass parameters directly to the constructor (not the recommended way):

  1. // enable handling of magic __call, __set but not __get:
  2. $propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL | PropertyAccessor::MAGIC_SET);

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.