Компонент PropertyAccess

Дата обновления перевода 2024-07-10

Компонент PropertyAccess

Компонент PropertyAccess предоставляет функцию для чтения и написания из/в объект или массив, используя простую нотацию строки.

Установка

1
$ composer require symfony/property-access

Note

Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно подключить файл vendor/autoload.php в вашем коде для включения механизма автозагрузки классов, предоставляемых Composer. Детальнее читайте в этой статье.

Использование

Входная точка этого компонента - это фабрика createPropertyAccessor(). Это фабрика создаст новый экземпляр класса PropertyAccessor с конфигурацией по умолчанию:

1
2
3
use Symfony\Component\PropertyAccess\PropertyAccess;

$propertyAccessor = PropertyAccess::createPropertyAccessor();

Чтение из массивов

Вы можете прочитать массив с помощью метода getValue(). Это делается, используя нотации индекса, которые используются в PHP:

1
2
3
4
5
6
7
// ...
$person = [
    'first_name' => 'Wouter',
];

var_dump($propertyAccessor->getValue($person, '[first_name]')); // 'Wouter'
var_dump($propertyAccessor->getValue($person, '[age]')); // null

Как вы можете увидеть, метод вернёт null, если индекс не существует. Но вы можете изменить это поведение с помощью метода enableExceptionOnInvalidIndex():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
    ->enableExceptionOnInvalidIndex()
    ->getPropertyAccessor();

$person = [
    'first_name' => 'Wouter',
];

// вместо возвращения null, теперь код вызывает исключение типа
// Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
$value = $propertyAccessor->getValue($person, '[age]');

// Вы можете избежать исключения, добавив оператор с защитой от null
$value = $propertyAccessor->getValue($person, '[age?]');

Вы также можете использовать многомерные массивы:

1
2
3
4
5
6
7
8
9
10
11
12
// ...
$persons = [
    [
        'first_name' => 'Wouter',
    ],
    [
        'first_name' => 'Ryan',
    ],
];

var_dump($propertyAccessor->getValue($persons, '[0][first_name]')); // 'Wouter'
var_dump($propertyAccessor->getValue($persons, '[1][first_name]')); // 'Ryan'

Tip

Если ключ массива содержит точку . или левую квадратную скобку [, то вам необходимо экранировать эти символы обратным слешем. В приведенном выше примере, если бы ключом массива было first.name, а не first_name, то к его значению следовало бы получить доступ таким образом:

1
2
var_dump($propertyAccessor->getValue($persons, '[0][first\.name]')); // 'Wouter'
var_dump($propertyAccessor->getValue($persons, '[1][first\.name]')); // 'Ryan'

Правые квадратные скобки ] не требуют экранирования в массиве ключей.

Чтение из объектов

Метод getValue() очень обширный, и вы можете увидеть все его функции при работе с объектами.

Доступ к публичным свойствам

Чтобы считывать из свойств, используйте нотацию "dot":

1
2
3
4
5
6
7
8
9
10
11
// ...
$person = new Person();
$person->firstName = 'Wouter';

var_dump($propertyAccessor->getValue($person, 'firstName')); // 'Wouter'

$child = new Person();
$child->firstName = 'Bar';
$person->children = [$child];

var_dump($propertyAccessor->getValue($person, 'children[0].firstName')); // 'Bar'

Caution

Доступ к публичным свойствам - это последня опция, используемая PropertyAccessor. Она пытается получить доступ к значению, используя методы описанные ниже до использования свойства напрямую. Например, если у вас есть публичное свойство, которое имеет метод геттера, то она будет использовать геттер.

Использование геттеров

Метод getValue() также поддерживает чтение, используя геттеры. Этот метод будет создан, используя общие соглашения об именовании для геттеров. Он преобразует имя свойства в camelCase (first_name становится FirstName) и добавляет к нему префикс get. Поэтому сам метод становится getFirstName():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...
class Person
{
    private string $firstName = 'Wouter';

    public function getFirstName(): string
    {
        return $this->firstName;
    }
}

$person = new Person();

var_dump($propertyAccessor->getValue($person, 'first_name')); // 'Wouter'

Использование хассеров / иссеров

На этом всё не останавливается. Если геттер не найден, процесс доступа будет искать иссер или хассер. Этот метод создается, используя тот же способ что и геттеры, что означает, что вы можете сделать что-то вроде этого:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...
class Person
{
    private bool $author = true;
    private array $children = [];

    public function isAuthor(): bool
    {
        return $this->author;
    }

    public function hasChildren(): bool
    {
        return 0 !== count($this->children);
    }
}

$person = new Person();

if ($propertyAccessor->getValue($person, 'author')) {
    var_dump('This person is an author');
}
if ($propertyAccessor->getValue($person, 'children')) {
    var_dump('This person has children');
}

Это произведёт: This person is an author

Доступ к несуществующему пути свойства

По умолчанию, если путь свойства, переданный getValue() не существует, вызывается NoSuchPropertyException. Вы можете изенить это поведение, используя метод disableExceptionOnInvalidPropertyPath():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...
class Person
{
    public string $name;
}

$person = new Person();

$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
    ->disableExceptionOnInvalidPropertyPath()
    ->getPropertyAccessor();

// вместо вызова исключения, следующий код вернет null
$value = $propertyAccessor->getValue($person, 'birthday');

Доступ к путям Null-свойств

Рассмотрите следующий PHP-код:

1
2
3
4
5
6
7
8
9
10
11
12
class Person
{
}

class Comment
{
    public ?Person $person = null;
    public string $message;
}

$comment = new Comment();
$comment->message = 'test';

Учитывая, что $person может быть null, граф объекта вроде comment.person.profile вызовет исключение, если свойство $person - null. Решением будет отметить все null-свойства с оператором защиты от null (?):

1
2
3
4
5
6
7
// Этот код вызывает исключение типа
// Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
var_dump($propertyAccessor->getValue($comment, 'person.firstname'));

// Если свойство, отмеченное оператором защиты от null, - null, выражение больше не
// будет оцениваться, а null будет возвращено немедленно, без вызова исключения
var_dump($propertyAccessor->getValue($comment, 'person?.firstname')); // null

Волшебный метод __get()

Метод getValue() может также использовать волшебный метод __get():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
class Person
{
    private array $children = [
        'Wouter' => [...],
    ];

    public function __get($id): mixed
    {
        return $this->children[$id];
    }
}

$person = new Person();

var_dump($propertyAccessor->getValue($person, 'Wouter')); // [...]

Note

Поддержка метода __get() включена по умолчанию. См. Включение других функций, если вы хотите её отключить.

Волшебный метод __call()

Наконец, getValue() может использовать волшебный метод __call(), но вам нужно включить эту функцию, используя PropertyAccessorBuilder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ...
class Person
{
    private array $children = [
        'wouter' => [...],
    ];

    public function __call($name, $args): mixed
    {
        $property = lcfirst(substr($name, 3));
        if ('get' === substr($name, 0, 3)) {
            return $this->children[$property] ?? null;
        } elseif ('set' === substr($name, 0, 3)) {
            $value = 1 == count($args) ? $args[0] : null;
            $this->children[$property] = $value;
        }
    }
}

$person = new Person();

// включает волшебный метод PHP __call()
$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
    ->enableMagicCall()
    ->getPropertyAccessor();

var_dump($propertyAccessor->getValue($person, 'wouter')); // [...]

Caution

Функция __call() отключена по умолчанию, вы можете включить её вызвав enableMagicCall() см. Включение других функций.

Запись в массивы

Класс PropertyAccessor может делать больше, чем просто читать массивы, он может также писать в массив. Этого можно достичь, используя метод setValue():

1
2
3
4
5
6
7
8
// ...
$person = [];

$propertyAccessor->setValue($person, '[first_name]', 'Wouter');

var_dump($propertyAccessor->getValue($person, '[first_name]')); // 'Wouter'
// или
// var_dump($person['first_name']); // 'Wouter'

Запись в объекты

Метод setValue() имеет такие же функции, как метод getValue(). Вы можете использовать сеттеры, волшебный метод __set() или свойства для установки значений:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ...
class Person
{
    public string $firstName;
    private string $lastName;
    private array $children = [];

    public function setLastName($name): void
    {
        $this->lastName = $name;
    }

    public function getLastName(): string
    {
        return $this->lastName;
    }

    public function getChildren(): array
    {
        return $this->children;
    }

    public function __set($property, $value): void
    {
        $this->$property = $value;
    }
}

$person = new Person();

$propertyAccessor->setValue($person, 'firstName', 'Wouter');
$propertyAccessor->setValue($person, 'lastName', 'de Jong'); // setLastName is called
$propertyAccessor->setValue($person, 'children', [new Person()]); // __set is called

var_dump($person->firstName); // 'Wouter'
var_dump($person->getLastName()); // 'de Jong'
var_dump($person->getChildren()); // [Person()];

Вы можете также использовать __call(), чтобы устанавливать значения, но вам нужно включить функцию, см. Включение других функций:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ...
class Person
{
    private array $children = [];

    public function __call($name, $args): mixed
    {
        $property = lcfirst(substr($name, 3));
        if ('get' === substr($name, 0, 3)) {
            return $this->children[$property] ?? null;
        } elseif ('set' === substr($name, 0, 3)) {
            $value = 1 == count($args) ? $args[0] : null;
            $this->children[$property] = $value;
        }
    }

}

$person = new Person();

// Enable magic __call
$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
    ->enableMagicCall()
    ->getPropertyAccessor();

$propertyAccessor->setValue($person, 'wouter', [...]);

var_dump($person->getWouter()); // [...]

Note

Поддержка метода __set() включена по умолчанию. См. Включение других функций, если вы хотите её отключить.

Запись в массив свойств

Класс PropertyAccessor позволяет обновлять содержание массивов, содержащихся в свойствах, через методы добавления и удаления (adder и remover):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ...
class Person
{
    /**
     * @var string[]
     */
    private array $children = [];

    public function getChildren(): array
    {
        return $this->children;
    }

    public function addChild(string $name): void
    {
        $this->children[$name] = $name;
    }

    public function removeChild(string $name): void
    {
        unset($this->children[$name]);
    }
}

$person = new Person();
$propertyAccessor->setValue($person, 'children', ['kevin', 'wouter']);

var_dump($person->getChildren()); // ['kevin', 'wouter']

Компонент PropertyAccess ищет методы под названием add<SingularOfThePropertyName>() и remove<SingularOfThePropertyName>(). Оба метода должны быть определены. Например, в предыдущем примере, компонент щет методы addChild() и removeChild(), чтобы получить доступ к свойству children. Компонент String используется, чтобы найти единственное число имени свойства.

Если они доступны, то методы добавления и удаления имеют приоритет перед методом сеттера.

Использование нестандартных методов добавления/удаления

Иногда методы добавления и удаления не используют стандартный префикс add или remove, Как в этом примере:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ...
class PeopleList
{
    // ...

    public function joinPeople(string $people): void
    {
        $this->peoples[] = $people;
    }

    public function leavePeople(string $people): void
    {
        foreach ($this->peoples as $id => $item) {
            if ($people === $item) {
                unset($this->peoples[$id]);
                break;
            }
        }
    }
}

use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyAccess\PropertyAccessor;

$list = new PeopleList();
$reflectionExtractor = new ReflectionExtractor(null, null, ['join', 'leave']);
$propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH, null, $reflectionExtractor, $reflectionExtractor);
$propertyAccessor->setValue($person, 'peoples', ['kevin', 'wouter']);

var_dump($person->getPeoples()); // ['kevin', 'wouter']

Вместо вызова add<SingularOfThePropertyName>() и remove<SingularOfThePropertyName>(), компонент PropertyAccess вызовет методы join<SingularOfThePropertyName>() и leave<SingularOfThePropertyName>().

Проверка путей свойства

Если вы хотите проверить может ли безопасно быть вызван, getValue() на самом деле не вызывая этот метод, то вы можете вместо этого использовать isReadable() instead:

1
2
3
4
5
$person = new Person();

if ($propertyAccessor->isReadable($person, 'firstName')) {
    // ...
}

То же самое возможно для setValue(): Вызовите метод isWritable(), чтобы узнать, можно ли обновить путь свойства:

1
2
3
4
5
$person = new Person();

if ($propertyAccessor->isWritable($person, 'firstName')) {
    // ...
}

Смешение объектов и массивов

Вы можете также смешивать объекты и массивы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ...
class Person
{
    public string $firstName;
    private array $children = [];

    public function setChildren($children): void
    {
        $this->children = $children;
    }

    public function getChildren(): array
    {
        return $this->children;
    }
}

$person = new Person();

$propertyAccessor->setValue($person, 'children[0]', new Person);
// равняется $person->getChildren()[0] = new Person()

$propertyAccessor->setValue($person, 'children[0].firstName', 'Wouter');
// равняется $person->getChildren()[0]->firstName = 'Wouter'

var_dump('Hello '.$propertyAccessor->getValue($person, 'children[0].firstName')); // 'Wouter'
// равняется $person->getChildren()[0]->firstName

Включение других функций

PropertyAccessor может быть сконфигурирован так, чтобы включать дополнительные функции. Чтобы сделать это, вы можете использовать PropertyAccessorBuilder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...
$propertyAccessorBuilder = PropertyAccess::createPropertyAccessorBuilder();

$propertyAccessorBuilder->enableMagicCall(); // включает волшебный __call
$propertyAccessorBuilder->enableMagicGet(); // включает волшебный __get
$propertyAccessorBuilder->enableMagicSet(); // включает волшебный __set
$propertyAccessorBuilder->enableMagicMethods(); // включает волшебные __get, __set и __call

$propertyAccessorBuilder->disableMagicCall(); // отключает волшебный __call
$propertyAccessorBuilder->disableMagicGet(); // отключает волшебный __get
$propertyAccessorBuilder->disableMagicSet(); // отключает волшебный __set
$propertyAccessorBuilder->disableMagicMethods(); // отключает волшебные __get, __set и __call

// проверяет, включена ли волшебная обработка __call, __get или __set
$propertyAccessorBuilder->isMagicCallEnabled(); // true or false
$propertyAccessorBuilder->isMagicGetEnabled(); // true or false
$propertyAccessorBuilder->isMagicSetEnabled(); // true or false

// В конце, получить сконфигурированный аксессор свойства
$propertyAccessor = $propertyAccessorBuilder->getPropertyAccessor();

// Или все сразу
$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
    ->enableMagicCall()
    ->getPropertyAccessor();

Или вы можете передать параметры напрямую в конструктор (не рекомендуется):

1
2
// включить обработку волшебных magic __call, __set но не __get:
$propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL | PropertyAccessor::MAGIC_SET);