├── Exception ├── AccessException.php ├── ExceptionInterface.php ├── NoSuchIndexException.php ├── NoSuchPropertyException.php ├── InvalidPropertyPathException.php ├── UninitializedPropertyException.php ├── RuntimeException.php ├── OutOfBoundsException.php ├── InvalidArgumentException.php ├── InvalidTypeException.php └── UnexpectedTypeException.php ├── README.md ├── PropertyPathIteratorInterface.php ├── PropertyAccess.php ├── composer.json ├── LICENSE ├── PropertyPathIterator.php ├── PropertyPathInterface.php ├── CHANGELOG.md ├── PropertyAccessorInterface.php ├── PropertyPath.php ├── PropertyAccessorBuilder.php ├── PropertyPathBuilder.php └── PropertyAccessor.php /Exception/AccessException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | /** 15 | * Thrown when a property path is not available. 16 | * 17 | * @author Stéphane Escandell 18 | */ 19 | class AccessException extends RuntimeException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | /** 15 | * Marker interface for the PropertyAccess component. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | interface ExceptionInterface extends \Throwable 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/NoSuchIndexException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | /** 15 | * Thrown when an index cannot be found. 16 | * 17 | * @author Stéphane Escandell 18 | */ 19 | class NoSuchIndexException extends AccessException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/NoSuchPropertyException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | /** 15 | * Thrown when a property cannot be found. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | class NoSuchPropertyException extends AccessException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/InvalidPropertyPathException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | /** 15 | * Thrown when a property path is malformed. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | class InvalidPropertyPathException extends RuntimeException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/UninitializedPropertyException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | /** 15 | * Thrown when a property is not initialized. 16 | * 17 | * @author Jules Pietri 18 | */ 19 | class UninitializedPropertyException extends AccessException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | /** 15 | * Base RuntimeException for the PropertyAccess component. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | class RuntimeException extends \RuntimeException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PropertyAccess Component 2 | ======================== 3 | 4 | The PropertyAccess component provides functions to read and write from/to an 5 | object or array using a simple string notation. 6 | 7 | Resources 8 | --------- 9 | 10 | * [Documentation](https://symfony.com/doc/current/components/property_access.html) 11 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 12 | * [Report issues](https://github.com/symfony/symfony/issues) and 13 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 14 | in the [main Symfony repository](https://github.com/symfony/symfony) 15 | -------------------------------------------------------------------------------- /Exception/OutOfBoundsException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | /** 15 | * Base OutOfBoundsException for the PropertyAccess component. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | /** 15 | * Base InvalidArgumentException for the PropertyAccess component. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /PropertyPathIteratorInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess; 13 | 14 | /** 15 | * @author Bernhard Schussek 16 | * 17 | * @extends \SeekableIterator 18 | */ 19 | interface PropertyPathIteratorInterface extends \SeekableIterator 20 | { 21 | /** 22 | * Returns whether the current element in the property path is an array 23 | * index. 24 | */ 25 | public function isIndex(): bool; 26 | 27 | /** 28 | * Returns whether the current element in the property path is a property 29 | * name. 30 | */ 31 | public function isProperty(): bool; 32 | } 33 | -------------------------------------------------------------------------------- /PropertyAccess.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess; 13 | 14 | /** 15 | * Entry point of the PropertyAccess component. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | final class PropertyAccess 20 | { 21 | /** 22 | * Creates a property accessor with the default configuration. 23 | */ 24 | public static function createPropertyAccessor(): PropertyAccessor 25 | { 26 | return self::createPropertyAccessorBuilder()->getPropertyAccessor(); 27 | } 28 | 29 | public static function createPropertyAccessorBuilder(): PropertyAccessorBuilder 30 | { 31 | return new PropertyAccessorBuilder(); 32 | } 33 | 34 | /** 35 | * This class cannot be instantiated. 36 | */ 37 | private function __construct() 38 | { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Exception/InvalidTypeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | /** 15 | * Thrown when a type of given value does not match an expected type. 16 | * 17 | * @author Farhad Safarov 18 | */ 19 | class InvalidTypeException extends InvalidArgumentException 20 | { 21 | public function __construct( 22 | public readonly string $expectedType, 23 | public readonly string $actualType, 24 | public readonly string $propertyPath, 25 | ?\Throwable $previous = null, 26 | ) { 27 | parent::__construct( 28 | \sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $expectedType, 'NULL' === $actualType ? 'null' : $actualType, $propertyPath), 29 | previous: $previous, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/property-access", 3 | "type": "library", 4 | "description": "Provides functions to read and write from/to an object or array using a simple string notation", 5 | "keywords": ["property", "index", "access", "object", "array", "extraction", "injection", "reflection", "property-path"], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "symfony/property-info": "^6.4.31|~7.3.9|^7.4.2" 21 | }, 22 | "require-dev": { 23 | "symfony/cache": "^6.4|^7.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { "Symfony\\Component\\PropertyAccess\\": "" }, 27 | "exclude-from-classmap": [ 28 | "/Tests/" 29 | ] 30 | }, 31 | "minimum-stability": "dev" 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /PropertyPathIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess; 13 | 14 | /** 15 | * Traverses a property path and provides additional methods to find out 16 | * information about the current element. 17 | * 18 | * @author Bernhard Schussek 19 | * 20 | * @extends \ArrayIterator 21 | */ 22 | class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface 23 | { 24 | public function __construct( 25 | protected PropertyPathInterface $path, 26 | ) { 27 | parent::__construct($path->getElements()); 28 | } 29 | 30 | public function isIndex(): bool 31 | { 32 | return $this->path->isIndex($this->key()); 33 | } 34 | 35 | public function isProperty(): bool 36 | { 37 | return $this->path->isProperty($this->key()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Exception/UnexpectedTypeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess\Exception; 13 | 14 | use Symfony\Component\PropertyAccess\PropertyPathInterface; 15 | 16 | /** 17 | * Thrown when a value does not match an expected type. 18 | * 19 | * @author Bernhard Schussek 20 | */ 21 | class UnexpectedTypeException extends RuntimeException 22 | { 23 | /** 24 | * @param mixed $value The unexpected value found while traversing property path 25 | * @param int $pathIndex The property path index when the unexpected value was found 26 | */ 27 | public function __construct(mixed $value, PropertyPathInterface $path, int $pathIndex) 28 | { 29 | $message = \sprintf( 30 | 'PropertyAccessor requires a graph of objects or arrays to operate on, '. 31 | 'but it found type "%s" while trying to traverse path "%s" at property "%s".', 32 | \gettype($value), 33 | (string) $path, 34 | $path->getElement($pathIndex) 35 | ); 36 | 37 | parent::__construct($message); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /PropertyPathInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess; 13 | 14 | /** 15 | * A sequence of property names or array indices. 16 | * 17 | * @author Bernhard Schussek 18 | * 19 | * @extends \Traversable 20 | */ 21 | interface PropertyPathInterface extends \Traversable, \Stringable 22 | { 23 | /** 24 | * Returns the string representation of the property path. 25 | */ 26 | public function __toString(): string; 27 | 28 | /** 29 | * Returns the length of the property path, i.e. the number of elements. 30 | */ 31 | public function getLength(): int; 32 | 33 | /** 34 | * Returns the parent property path. 35 | * 36 | * The parent property path is the one that contains the same items as 37 | * this one except for the last one. 38 | * 39 | * If this property path only contains one item, null is returned. 40 | */ 41 | public function getParent(): ?self; 42 | 43 | /** 44 | * Returns the elements of the property path as array. 45 | * 46 | * @return list 47 | */ 48 | public function getElements(): array; 49 | 50 | /** 51 | * Returns the element at the given index in the property path. 52 | * 53 | * @param int $index The index key 54 | * 55 | * @throws Exception\OutOfBoundsException If the offset is invalid 56 | */ 57 | public function getElement(int $index): string; 58 | 59 | /** 60 | * Returns whether the element at the given index is a property. 61 | * 62 | * @param int $index The index in the property path 63 | * 64 | * @throws Exception\OutOfBoundsException If the offset is invalid 65 | */ 66 | public function isProperty(int $index): bool; 67 | 68 | /** 69 | * Returns whether the element at the given index is an array index. 70 | * 71 | * @param int $index The index in the property path 72 | * 73 | * @throws Exception\OutOfBoundsException If the offset is invalid 74 | */ 75 | public function isIndex(int $index): bool; 76 | 77 | /** 78 | * Returns whether the element at the given index is null safe. 79 | */ 80 | public function isNullSafe(int $index): bool; 81 | } 82 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.0 5 | --- 6 | 7 | * Add method `isNullSafe()` to `PropertyPathInterface` 8 | * Require explicit argument when calling `PropertyAccessorBuilder::setCacheItemPool()` 9 | 10 | 6.3 11 | --- 12 | 13 | * Allow escaping `.` and `[` with `\` in `PropertyPath` 14 | 15 | 6.2 16 | --- 17 | 18 | * Deprecate calling `PropertyAccessorBuilder::setCacheItemPool()` without arguments 19 | * Added method `isNullSafe()` to `PropertyPathInterface`, implementing the interface without implementing this method 20 | is deprecated 21 | * Add support for the null-coalesce operator in property paths 22 | 23 | 6.0 24 | --- 25 | 26 | * make `PropertyAccessor::__construct()` accept a combination of bitwise flags as first and second arguments 27 | 28 | 5.3.0 29 | ----- 30 | 31 | * deprecate passing a boolean as the second argument of `PropertyAccessor::__construct()`, expecting a combination of bitwise flags instead 32 | 33 | 5.2.0 34 | ----- 35 | 36 | * deprecated passing a boolean as the first argument of `PropertyAccessor::__construct()`, expecting a combination of bitwise flags instead 37 | * added the ability to disable usage of the magic `__get` & `__set` methods 38 | 39 | 5.1.0 40 | ----- 41 | 42 | * Added an `UninitializedPropertyException` 43 | * Linking to PropertyInfo extractor to remove a lot of duplicate code 44 | 45 | 4.4.0 46 | ----- 47 | 48 | * deprecated passing `null` as `$defaultLifetime` 2nd argument of `PropertyAccessor::createCache()` method, 49 | pass `0` instead 50 | 51 | 4.3.0 52 | ----- 53 | 54 | * added a `$throwExceptionOnInvalidPropertyPath` argument to the PropertyAccessor constructor. 55 | * added `enableExceptionOnInvalidPropertyPath()`, `disableExceptionOnInvalidPropertyPath()` and 56 | `isExceptionOnInvalidPropertyPath()` methods to `PropertyAccessorBuilder` 57 | 58 | 4.0.0 59 | ----- 60 | 61 | * removed the `StringUtil` class, use `Symfony\Component\Inflector\Inflector` 62 | 63 | 3.1.0 64 | ----- 65 | 66 | * deprecated the `StringUtil` class, use `Symfony\Component\Inflector\Inflector` 67 | instead 68 | 69 | 2.7.0 70 | ------ 71 | 72 | * `UnexpectedTypeException` now expects three constructor arguments: The invalid property value, 73 | the `PropertyPathInterface` object and the current index of the property path. 74 | 75 | 2.5.0 76 | ------ 77 | 78 | * allowed non alpha numeric characters in second level and deeper object properties names 79 | * [BC BREAK] when accessing an index on an object that does not implement 80 | ArrayAccess, a NoSuchIndexException is now thrown instead of the 81 | semantically wrong NoSuchPropertyException 82 | * [BC BREAK] added isReadable() and isWritable() to PropertyAccessorInterface 83 | 84 | 2.3.0 85 | ------ 86 | 87 | * added PropertyAccessorBuilder, to enable or disable the support of "__call" 88 | * added support for "__call" in the PropertyAccessor (disabled by default) 89 | * [BC BREAK] changed PropertyAccessor to continue its search for a property or 90 | method even if a non-public match was found. Before, a PropertyAccessDeniedException 91 | was thrown in this case. Class PropertyAccessDeniedException was removed 92 | now. 93 | * deprecated PropertyAccess::getPropertyAccessor 94 | * added PropertyAccess::createPropertyAccessor and PropertyAccess::createPropertyAccessorBuilder 95 | -------------------------------------------------------------------------------- /PropertyAccessorInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess; 13 | 14 | /** 15 | * Writes and reads values to/from an object/array graph. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | interface PropertyAccessorInterface 20 | { 21 | /** 22 | * Sets the value at the end of the property path of the object graph. 23 | * 24 | * Example: 25 | * 26 | * use Symfony\Component\PropertyAccess\PropertyAccess; 27 | * 28 | * $propertyAccessor = PropertyAccess::createPropertyAccessor(); 29 | * 30 | * echo $propertyAccessor->setValue($object, 'child.name', 'Fabien'); 31 | * // equals echo $object->getChild()->setName('Fabien'); 32 | * 33 | * This method first tries to find a public setter for each property in the 34 | * path. The name of the setter must be the camel-cased property name 35 | * prefixed with "set". 36 | * 37 | * If the setter does not exist, this method tries to find a public 38 | * property. The value of the property is then changed. 39 | * 40 | * If neither is found, an exception is thrown. 41 | * 42 | * @template T of object|array 43 | * 44 | * @param T $objectOrArray 45 | * 46 | * @param-out ($objectOrArray is array ? array : T) $objectOrArray 47 | * 48 | * @throws Exception\InvalidArgumentException If the property path is invalid 49 | * @throws Exception\AccessException If a property/index does not exist or is not public 50 | * @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array 51 | */ 52 | public function setValue(object|array &$objectOrArray, string|PropertyPathInterface $propertyPath, mixed $value): void; 53 | 54 | /** 55 | * Returns the value at the end of the property path of the object graph. 56 | * 57 | * Example: 58 | * 59 | * use Symfony\Component\PropertyAccess\PropertyAccess; 60 | * 61 | * $propertyAccessor = PropertyAccess::createPropertyAccessor(); 62 | * 63 | * echo $propertyAccessor->getValue($object, 'child.name'); 64 | * // equals echo $object->getChild()->getName(); 65 | * 66 | * This method first tries to find a public getter for each property in the 67 | * path. The name of the getter must be the camel-cased property name 68 | * prefixed with "get", "is", or "has". 69 | * 70 | * If the getter does not exist, this method tries to find a public 71 | * property. The value of the property is then returned. 72 | * 73 | * If none of them are found, an exception is thrown. 74 | * 75 | * @throws Exception\InvalidArgumentException If the property path is invalid 76 | * @throws Exception\AccessException If a property/index does not exist or is not public 77 | * @throws Exception\UnexpectedTypeException If a value within the path is neither object 78 | * nor array 79 | */ 80 | public function getValue(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): mixed; 81 | 82 | /** 83 | * Returns whether a value can be written at a given property path. 84 | * 85 | * Whenever this method returns true, {@link setValue()} is guaranteed not 86 | * to throw an exception when called with the same arguments. 87 | * 88 | * @throws Exception\InvalidArgumentException If the property path is invalid 89 | */ 90 | public function isWritable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool; 91 | 92 | /** 93 | * Returns whether a property path can be read from an object graph. 94 | * 95 | * Whenever this method returns true, {@link getValue()} is guaranteed not 96 | * to throw an exception when called with the same arguments. 97 | * 98 | * @throws Exception\InvalidArgumentException If the property path is invalid 99 | */ 100 | public function isReadable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool; 101 | } 102 | -------------------------------------------------------------------------------- /PropertyPath.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess; 13 | 14 | use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; 15 | use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException; 16 | use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException; 17 | 18 | /** 19 | * Default implementation of {@link PropertyPathInterface}. 20 | * 21 | * @author Bernhard Schussek 22 | * 23 | * @implements \IteratorAggregate 24 | */ 25 | class PropertyPath implements \IteratorAggregate, PropertyPathInterface 26 | { 27 | /** 28 | * Character used for separating between plural and singular of an element. 29 | */ 30 | public const SINGULAR_SEPARATOR = '|'; 31 | 32 | /** 33 | * The elements of the property path. 34 | * 35 | * @var list 36 | */ 37 | private array $elements = []; 38 | 39 | /** 40 | * The number of elements in the property path. 41 | */ 42 | private int $length; 43 | 44 | /** 45 | * Contains a Boolean for each property in $elements denoting whether this 46 | * element is an index. It is a property otherwise. 47 | * 48 | * @var array 49 | */ 50 | private array $isIndex = []; 51 | 52 | /** 53 | * Contains a Boolean for each property in $elements denoting whether this 54 | * element is optional or not. 55 | * 56 | * @var array 57 | */ 58 | private array $isNullSafe = []; 59 | 60 | /** 61 | * String representation of the path. 62 | */ 63 | private string $pathAsString; 64 | 65 | /** 66 | * Constructs a property path from a string. 67 | * 68 | * @throws InvalidArgumentException If the given path is not a string 69 | * @throws InvalidPropertyPathException If the syntax of the property path is not valid 70 | */ 71 | public function __construct(self|string $propertyPath) 72 | { 73 | // Can be used as copy constructor 74 | if ($propertyPath instanceof self) { 75 | $this->elements = $propertyPath->elements; 76 | $this->length = $propertyPath->length; 77 | $this->isIndex = $propertyPath->isIndex; 78 | $this->isNullSafe = $propertyPath->isNullSafe; 79 | $this->pathAsString = $propertyPath->pathAsString; 80 | 81 | return; 82 | } 83 | 84 | if ('' === $propertyPath) { 85 | throw new InvalidPropertyPathException('The property path should not be empty.'); 86 | } 87 | 88 | $this->pathAsString = $propertyPath; 89 | $position = 0; 90 | $remaining = $propertyPath; 91 | 92 | // first element is evaluated differently - no leading dot for properties 93 | $pattern = '/^(((?:[^\\\\.\[]|\\\\.)++)|\[([^\]]++)\])(.*)/'; 94 | 95 | while (preg_match($pattern, $remaining, $matches)) { 96 | if ('' !== $matches[2]) { 97 | $element = $matches[2]; 98 | $this->isIndex[] = false; 99 | } else { 100 | $element = $matches[3]; 101 | $this->isIndex[] = true; 102 | } 103 | 104 | // Mark as optional when last character is "?". 105 | if (str_ends_with($element, '?')) { 106 | $this->isNullSafe[] = true; 107 | $element = substr($element, 0, -1); 108 | } else { 109 | $this->isNullSafe[] = false; 110 | } 111 | 112 | $element = preg_replace('/\\\([.[])/', '$1', $element); 113 | if (str_ends_with($element, '\\\\')) { 114 | $element = substr($element, 0, -1); 115 | } 116 | $this->elements[] = $element; 117 | 118 | $position += \strlen($matches[1]); 119 | $remaining = $matches[4]; 120 | $pattern = '/^(\.((?:[^\\\\.\[]|\\\\.)++)|\[([^\]]++)\])(.*)/'; 121 | } 122 | 123 | if ('' !== $remaining) { 124 | throw new InvalidPropertyPathException(\sprintf('Could not parse property path "%s". Unexpected token "%s" at position %d.', $propertyPath, $remaining[0], $position)); 125 | } 126 | 127 | $this->length = \count($this->elements); 128 | } 129 | 130 | public function __toString(): string 131 | { 132 | return $this->pathAsString; 133 | } 134 | 135 | public function getLength(): int 136 | { 137 | return $this->length; 138 | } 139 | 140 | public function getParent(): ?PropertyPathInterface 141 | { 142 | if ($this->length <= 1) { 143 | return null; 144 | } 145 | 146 | $parent = clone $this; 147 | 148 | --$parent->length; 149 | $parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '['))); 150 | array_pop($parent->elements); 151 | array_pop($parent->isIndex); 152 | array_pop($parent->isNullSafe); 153 | 154 | return $parent; 155 | } 156 | 157 | /** 158 | * Returns a new iterator for this path. 159 | */ 160 | public function getIterator(): PropertyPathIteratorInterface 161 | { 162 | return new PropertyPathIterator($this); 163 | } 164 | 165 | public function getElements(): array 166 | { 167 | return $this->elements; 168 | } 169 | 170 | public function getElement(int $index): string 171 | { 172 | if (!isset($this->elements[$index])) { 173 | throw new OutOfBoundsException(\sprintf('The index "%s" is not within the property path.', $index)); 174 | } 175 | 176 | return $this->elements[$index]; 177 | } 178 | 179 | public function isProperty(int $index): bool 180 | { 181 | if (!isset($this->isIndex[$index])) { 182 | throw new OutOfBoundsException(\sprintf('The index "%s" is not within the property path.', $index)); 183 | } 184 | 185 | return !$this->isIndex[$index]; 186 | } 187 | 188 | public function isIndex(int $index): bool 189 | { 190 | if (!isset($this->isIndex[$index])) { 191 | throw new OutOfBoundsException(\sprintf('The index "%s" is not within the property path.', $index)); 192 | } 193 | 194 | return $this->isIndex[$index]; 195 | } 196 | 197 | public function isNullSafe(int $index): bool 198 | { 199 | if (!isset($this->isNullSafe[$index])) { 200 | throw new OutOfBoundsException(\sprintf('The index "%s" is not within the property path.', $index)); 201 | } 202 | 203 | return $this->isNullSafe[$index]; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /PropertyAccessorBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; 16 | use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; 17 | 18 | /** 19 | * A configurable builder to create a PropertyAccessor. 20 | * 21 | * @author Jérémie Augustin 22 | */ 23 | class PropertyAccessorBuilder 24 | { 25 | private int $magicMethods = PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_SET; 26 | private bool $throwExceptionOnInvalidIndex = false; 27 | private bool $throwExceptionOnInvalidPropertyPath = true; 28 | private ?CacheItemPoolInterface $cacheItemPool = null; 29 | private ?PropertyReadInfoExtractorInterface $readInfoExtractor = null; 30 | private ?PropertyWriteInfoExtractorInterface $writeInfoExtractor = null; 31 | 32 | /** 33 | * Enables the use of all magic methods by the PropertyAccessor. 34 | * 35 | * @return $this 36 | */ 37 | public function enableMagicMethods(): static 38 | { 39 | $this->magicMethods = PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_SET | PropertyAccessor::MAGIC_CALL; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * Disable the use of all magic methods by the PropertyAccessor. 46 | * 47 | * @return $this 48 | */ 49 | public function disableMagicMethods(): static 50 | { 51 | $this->magicMethods = PropertyAccessor::DISALLOW_MAGIC_METHODS; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Enables the use of "__call" by the PropertyAccessor. 58 | * 59 | * @return $this 60 | */ 61 | public function enableMagicCall(): static 62 | { 63 | $this->magicMethods |= PropertyAccessor::MAGIC_CALL; 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Enables the use of "__get" by the PropertyAccessor. 70 | */ 71 | public function enableMagicGet(): self 72 | { 73 | $this->magicMethods |= PropertyAccessor::MAGIC_GET; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Enables the use of "__set" by the PropertyAccessor. 80 | * 81 | * @return $this 82 | */ 83 | public function enableMagicSet(): static 84 | { 85 | $this->magicMethods |= PropertyAccessor::MAGIC_SET; 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Disables the use of "__call" by the PropertyAccessor. 92 | * 93 | * @return $this 94 | */ 95 | public function disableMagicCall(): static 96 | { 97 | $this->magicMethods &= ~PropertyAccessor::MAGIC_CALL; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Disables the use of "__get" by the PropertyAccessor. 104 | * 105 | * @return $this 106 | */ 107 | public function disableMagicGet(): static 108 | { 109 | $this->magicMethods &= ~PropertyAccessor::MAGIC_GET; 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Disables the use of "__set" by the PropertyAccessor. 116 | * 117 | * @return $this 118 | */ 119 | public function disableMagicSet(): static 120 | { 121 | $this->magicMethods &= ~PropertyAccessor::MAGIC_SET; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * @return bool whether the use of "__call" by the PropertyAccessor is enabled 128 | */ 129 | public function isMagicCallEnabled(): bool 130 | { 131 | return $this->magicMethods & PropertyAccessor::MAGIC_CALL; 132 | } 133 | 134 | /** 135 | * @return bool whether the use of "__get" by the PropertyAccessor is enabled 136 | */ 137 | public function isMagicGetEnabled(): bool 138 | { 139 | return $this->magicMethods & PropertyAccessor::MAGIC_GET; 140 | } 141 | 142 | /** 143 | * @return bool whether the use of "__set" by the PropertyAccessor is enabled 144 | */ 145 | public function isMagicSetEnabled(): bool 146 | { 147 | return $this->magicMethods & PropertyAccessor::MAGIC_SET; 148 | } 149 | 150 | /** 151 | * Enables exceptions when reading a non-existing index. 152 | * 153 | * This has no influence on writing non-existing indices with PropertyAccessorInterface::setValue() 154 | * which are always created on-the-fly. 155 | * 156 | * @return $this 157 | */ 158 | public function enableExceptionOnInvalidIndex(): static 159 | { 160 | $this->throwExceptionOnInvalidIndex = true; 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * Disables exceptions when reading a non-existing index. 167 | * 168 | * Instead, null is returned when calling PropertyAccessorInterface::getValue() on a non-existing index. 169 | * 170 | * @return $this 171 | */ 172 | public function disableExceptionOnInvalidIndex(): static 173 | { 174 | $this->throwExceptionOnInvalidIndex = false; 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * @return bool whether an exception is thrown or null is returned when reading a non-existing index 181 | */ 182 | public function isExceptionOnInvalidIndexEnabled(): bool 183 | { 184 | return $this->throwExceptionOnInvalidIndex; 185 | } 186 | 187 | /** 188 | * Enables exceptions when reading a non-existing property. 189 | * 190 | * This has no influence on writing non-existing indices with PropertyAccessorInterface::setValue() 191 | * which are always created on-the-fly. 192 | * 193 | * @return $this 194 | */ 195 | public function enableExceptionOnInvalidPropertyPath(): static 196 | { 197 | $this->throwExceptionOnInvalidPropertyPath = true; 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * Disables exceptions when reading a non-existing index. 204 | * 205 | * Instead, null is returned when calling PropertyAccessorInterface::getValue() on a non-existing index. 206 | * 207 | * @return $this 208 | */ 209 | public function disableExceptionOnInvalidPropertyPath(): static 210 | { 211 | $this->throwExceptionOnInvalidPropertyPath = false; 212 | 213 | return $this; 214 | } 215 | 216 | /** 217 | * @return bool whether an exception is thrown or null is returned when reading a non-existing property 218 | */ 219 | public function isExceptionOnInvalidPropertyPath(): bool 220 | { 221 | return $this->throwExceptionOnInvalidPropertyPath; 222 | } 223 | 224 | /** 225 | * Sets a cache system. 226 | * 227 | * @return $this 228 | */ 229 | public function setCacheItemPool(?CacheItemPoolInterface $cacheItemPool): static 230 | { 231 | $this->cacheItemPool = $cacheItemPool; 232 | 233 | return $this; 234 | } 235 | 236 | /** 237 | * Gets the used cache system. 238 | */ 239 | public function getCacheItemPool(): ?CacheItemPoolInterface 240 | { 241 | return $this->cacheItemPool; 242 | } 243 | 244 | /** 245 | * @return $this 246 | */ 247 | public function setReadInfoExtractor(?PropertyReadInfoExtractorInterface $readInfoExtractor): static 248 | { 249 | $this->readInfoExtractor = $readInfoExtractor; 250 | 251 | return $this; 252 | } 253 | 254 | public function getReadInfoExtractor(): ?PropertyReadInfoExtractorInterface 255 | { 256 | return $this->readInfoExtractor; 257 | } 258 | 259 | /** 260 | * @return $this 261 | */ 262 | public function setWriteInfoExtractor(?PropertyWriteInfoExtractorInterface $writeInfoExtractor): static 263 | { 264 | $this->writeInfoExtractor = $writeInfoExtractor; 265 | 266 | return $this; 267 | } 268 | 269 | public function getWriteInfoExtractor(): ?PropertyWriteInfoExtractorInterface 270 | { 271 | return $this->writeInfoExtractor; 272 | } 273 | 274 | /** 275 | * Builds and returns a new PropertyAccessor object. 276 | */ 277 | public function getPropertyAccessor(): PropertyAccessorInterface 278 | { 279 | $throw = PropertyAccessor::DO_NOT_THROW; 280 | 281 | if ($this->throwExceptionOnInvalidIndex) { 282 | $throw |= PropertyAccessor::THROW_ON_INVALID_INDEX; 283 | } 284 | 285 | if ($this->throwExceptionOnInvalidPropertyPath) { 286 | $throw |= PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH; 287 | } 288 | 289 | return new PropertyAccessor($this->magicMethods, $throw, $this->cacheItemPool, $this->readInfoExtractor, $this->writeInfoExtractor); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /PropertyPathBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess; 13 | 14 | use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException; 15 | 16 | /** 17 | * @author Bernhard Schussek 18 | */ 19 | class PropertyPathBuilder 20 | { 21 | private array $elements = []; 22 | private array $isIndex = []; 23 | 24 | public function __construct(PropertyPathInterface|string|null $path = null) 25 | { 26 | if (null !== $path) { 27 | $this->append($path); 28 | } 29 | } 30 | 31 | /** 32 | * Appends a (sub-) path to the current path. 33 | * 34 | * @param int $offset The offset where the appended piece starts in $path 35 | * @param int $length The length of the appended piece; if 0, the full path is appended 36 | */ 37 | public function append(PropertyPathInterface|string $path, int $offset = 0, int $length = 0): void 38 | { 39 | if (\is_string($path)) { 40 | $path = new PropertyPath($path); 41 | } 42 | 43 | if (0 === $length) { 44 | $end = $path->getLength(); 45 | } else { 46 | $end = $offset + $length; 47 | } 48 | 49 | for (; $offset < $end; ++$offset) { 50 | $this->elements[] = $path->getElement($offset); 51 | $this->isIndex[] = $path->isIndex($offset); 52 | } 53 | } 54 | 55 | /** 56 | * Appends an index element to the current path. 57 | */ 58 | public function appendIndex(string $name): void 59 | { 60 | $this->elements[] = $name; 61 | $this->isIndex[] = true; 62 | } 63 | 64 | /** 65 | * Appends a property element to the current path. 66 | */ 67 | public function appendProperty(string $name): void 68 | { 69 | $this->elements[] = $name; 70 | $this->isIndex[] = false; 71 | } 72 | 73 | /** 74 | * Removes elements from the current path. 75 | * 76 | * @throws OutOfBoundsException if offset is invalid 77 | */ 78 | public function remove(int $offset, int $length = 1): void 79 | { 80 | if (!isset($this->elements[$offset])) { 81 | throw new OutOfBoundsException(\sprintf('The offset "%s" is not within the property path.', $offset)); 82 | } 83 | 84 | $this->resize($offset, $length, 0); 85 | } 86 | 87 | /** 88 | * Replaces a sub-path by a different (sub-) path. 89 | * 90 | * @param int $pathOffset The offset where the inserted piece starts in $path 91 | * @param int $pathLength The length of the inserted piece; if 0, the full path is inserted 92 | * 93 | * @throws OutOfBoundsException If the offset is invalid 94 | */ 95 | public function replace(int $offset, int $length, PropertyPathInterface|string $path, int $pathOffset = 0, int $pathLength = 0): void 96 | { 97 | if (\is_string($path)) { 98 | $path = new PropertyPath($path); 99 | } 100 | 101 | if ($offset < 0 && abs($offset) <= $this->getLength()) { 102 | $offset = $this->getLength() + $offset; 103 | } elseif (!isset($this->elements[$offset])) { 104 | throw new OutOfBoundsException('The offset '.$offset.' is not within the property path'); 105 | } 106 | 107 | if (0 === $pathLength) { 108 | $pathLength = $path->getLength() - $pathOffset; 109 | } 110 | 111 | $this->resize($offset, $length, $pathLength); 112 | 113 | for ($i = 0; $i < $pathLength; ++$i) { 114 | $this->elements[$offset + $i] = $path->getElement($pathOffset + $i); 115 | $this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i); 116 | } 117 | ksort($this->elements); 118 | } 119 | 120 | /** 121 | * Replaces a property element by an index element. 122 | * 123 | * @throws OutOfBoundsException If the offset is invalid 124 | */ 125 | public function replaceByIndex(int $offset, ?string $name = null): void 126 | { 127 | if (!isset($this->elements[$offset])) { 128 | throw new OutOfBoundsException(\sprintf('The offset "%s" is not within the property path.', $offset)); 129 | } 130 | 131 | if (null !== $name) { 132 | $this->elements[$offset] = $name; 133 | } 134 | 135 | $this->isIndex[$offset] = true; 136 | } 137 | 138 | /** 139 | * Replaces an index element by a property element. 140 | * 141 | * @throws OutOfBoundsException If the offset is invalid 142 | */ 143 | public function replaceByProperty(int $offset, ?string $name = null): void 144 | { 145 | if (!isset($this->elements[$offset])) { 146 | throw new OutOfBoundsException(\sprintf('The offset "%s" is not within the property path.', $offset)); 147 | } 148 | 149 | if (null !== $name) { 150 | $this->elements[$offset] = $name; 151 | } 152 | 153 | $this->isIndex[$offset] = false; 154 | } 155 | 156 | /** 157 | * Returns the length of the current path. 158 | */ 159 | public function getLength(): int 160 | { 161 | return \count($this->elements); 162 | } 163 | 164 | /** 165 | * Returns the current property path. 166 | */ 167 | public function getPropertyPath(): ?PropertyPathInterface 168 | { 169 | $pathAsString = $this->__toString(); 170 | 171 | return '' !== $pathAsString ? new PropertyPath($pathAsString) : null; 172 | } 173 | 174 | /** 175 | * Returns the current property path as string. 176 | */ 177 | public function __toString(): string 178 | { 179 | $string = ''; 180 | 181 | foreach ($this->elements as $offset => $element) { 182 | if ($this->isIndex[$offset]) { 183 | $element = '['.$element.']'; 184 | } elseif ('' !== $string) { 185 | $string .= '.'; 186 | } 187 | 188 | $string .= $element; 189 | } 190 | 191 | return $string; 192 | } 193 | 194 | /** 195 | * Resizes the path so that a chunk of length $cutLength is 196 | * removed at $offset and another chunk of length $insertionLength 197 | * can be inserted. 198 | */ 199 | private function resize(int $offset, int $cutLength, int $insertionLength): void 200 | { 201 | // Nothing else to do in this case 202 | if ($insertionLength === $cutLength) { 203 | return; 204 | } 205 | 206 | $length = \count($this->elements); 207 | 208 | if ($cutLength > $insertionLength) { 209 | // More elements should be removed than inserted 210 | $diff = $cutLength - $insertionLength; 211 | $newLength = $length - $diff; 212 | 213 | // Shift elements to the left (left-to-right until the new end) 214 | // Max allowed offset to be shifted is such that 215 | // $offset + $diff < $length (otherwise invalid index access) 216 | // i.e. $offset < $length - $diff = $newLength 217 | for ($i = $offset; $i < $newLength; ++$i) { 218 | $this->elements[$i] = $this->elements[$i + $diff]; 219 | $this->isIndex[$i] = $this->isIndex[$i + $diff]; 220 | } 221 | 222 | // All remaining elements should be removed 223 | $this->elements = \array_slice($this->elements, 0, $i); 224 | $this->isIndex = \array_slice($this->isIndex, 0, $i); 225 | } else { 226 | $diff = $insertionLength - $cutLength; 227 | 228 | $newLength = $length + $diff; 229 | $indexAfterInsertion = $offset + $insertionLength; 230 | 231 | // $diff <= $insertionLength 232 | // $indexAfterInsertion >= $insertionLength 233 | // => $diff <= $indexAfterInsertion 234 | 235 | // In each of the following loops, $i >= $diff must hold, 236 | // otherwise ($i - $diff) becomes negative. 237 | 238 | // Shift old elements to the right to make up space for the 239 | // inserted elements. This needs to be done left-to-right in 240 | // order to preserve an ascending array index order 241 | // Since $i = max($length, $indexAfterInsertion) and $indexAfterInsertion >= $diff, 242 | // $i >= $diff is guaranteed. 243 | for ($i = max($length, $indexAfterInsertion); $i < $newLength; ++$i) { 244 | $this->elements[$i] = $this->elements[$i - $diff]; 245 | $this->isIndex[$i] = $this->isIndex[$i - $diff]; 246 | } 247 | 248 | // Shift remaining elements to the right. Do this right-to-left 249 | // so we don't overwrite elements before copying them 250 | // The last written index is the immediate index after the inserted 251 | // string, because the indices before that will be overwritten 252 | // anyway. 253 | // Since $i >= $indexAfterInsertion and $indexAfterInsertion >= $diff, 254 | // $i >= $diff is guaranteed. 255 | for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) { 256 | $this->elements[$i] = $this->elements[$i - $diff]; 257 | $this->isIndex[$i] = $this->isIndex[$i - $diff]; 258 | } 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /PropertyAccessor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\PropertyAccess; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Psr\Log\NullLogger; 17 | use Symfony\Component\Cache\Adapter\AdapterInterface; 18 | use Symfony\Component\Cache\Adapter\ApcuAdapter; 19 | use Symfony\Component\Cache\Adapter\NullAdapter; 20 | use Symfony\Component\PropertyAccess\Exception\AccessException; 21 | use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; 22 | use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; 23 | use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; 24 | use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; 25 | use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; 26 | use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; 27 | use Symfony\Component\PropertyInfo\PropertyReadInfo; 28 | use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; 29 | use Symfony\Component\PropertyInfo\PropertyWriteInfo; 30 | use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; 31 | 32 | /** 33 | * Default implementation of {@link PropertyAccessorInterface}. 34 | * 35 | * @author Bernhard Schussek 36 | * @author Kévin Dunglas 37 | * @author Nicolas Grekas 38 | */ 39 | class PropertyAccessor implements PropertyAccessorInterface 40 | { 41 | /** @var int Allow none of the magic methods */ 42 | public const DISALLOW_MAGIC_METHODS = ReflectionExtractor::DISALLOW_MAGIC_METHODS; 43 | /** @var int Allow magic __get methods */ 44 | public const MAGIC_GET = ReflectionExtractor::ALLOW_MAGIC_GET; 45 | /** @var int Allow magic __set methods */ 46 | public const MAGIC_SET = ReflectionExtractor::ALLOW_MAGIC_SET; 47 | /** @var int Allow magic __call methods */ 48 | public const MAGIC_CALL = ReflectionExtractor::ALLOW_MAGIC_CALL; 49 | 50 | public const DO_NOT_THROW = 0; 51 | public const THROW_ON_INVALID_INDEX = 1; 52 | public const THROW_ON_INVALID_PROPERTY_PATH = 2; 53 | 54 | private const VALUE = 0; 55 | private const REF = 1; 56 | private const IS_REF_CHAINED = 2; 57 | private const CACHE_PREFIX_READ = 'r'; 58 | private const CACHE_PREFIX_WRITE = 'w'; 59 | private const CACHE_PREFIX_PROPERTY_PATH = 'p'; 60 | private const RESULT_PROTO = [self::VALUE => null]; 61 | 62 | private bool $ignoreInvalidIndices; 63 | private bool $ignoreInvalidProperty; 64 | private ?CacheItemPoolInterface $cacheItemPool; 65 | private array $propertyPathCache = []; 66 | private PropertyReadInfoExtractorInterface $readInfoExtractor; 67 | private PropertyWriteInfoExtractorInterface $writeInfoExtractor; 68 | private array $readPropertyCache = []; 69 | private array $writePropertyCache = []; 70 | 71 | /** 72 | * Should not be used by application code. Use 73 | * {@link PropertyAccess::createPropertyAccessor()} instead. 74 | * 75 | * @param int $magicMethodsFlags A bitwise combination of the MAGIC_* constants 76 | * to specify the allowed magic methods (__get, __set, __call) 77 | * or self::DISALLOW_MAGIC_METHODS for none 78 | * @param int $throw A bitwise combination of the THROW_* constants 79 | * to specify when exceptions should be thrown 80 | */ 81 | public function __construct( 82 | private int $magicMethodsFlags = self::MAGIC_GET | self::MAGIC_SET, 83 | int $throw = self::THROW_ON_INVALID_PROPERTY_PATH, 84 | ?CacheItemPoolInterface $cacheItemPool = null, 85 | ?PropertyReadInfoExtractorInterface $readInfoExtractor = null, 86 | ?PropertyWriteInfoExtractorInterface $writeInfoExtractor = null, 87 | ) { 88 | $this->ignoreInvalidIndices = 0 === ($throw & self::THROW_ON_INVALID_INDEX); 89 | $this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value 90 | $this->ignoreInvalidProperty = 0 === ($throw & self::THROW_ON_INVALID_PROPERTY_PATH); 91 | $this->readInfoExtractor = $readInfoExtractor ?? new ReflectionExtractor([], null, null, false); 92 | $this->writeInfoExtractor = $writeInfoExtractor ?? new ReflectionExtractor(['set'], null, null, false); 93 | } 94 | 95 | public function getValue(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): mixed 96 | { 97 | $zval = [ 98 | self::VALUE => $objectOrArray, 99 | ]; 100 | 101 | if (\is_object($objectOrArray) && (false === strpbrk((string) $propertyPath, '.[?') || $objectOrArray instanceof \stdClass && property_exists($objectOrArray, $propertyPath))) { 102 | return $this->readProperty($zval, $propertyPath, $this->ignoreInvalidProperty)[self::VALUE]; 103 | } 104 | 105 | $propertyPath = $this->getPropertyPath($propertyPath); 106 | 107 | $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); 108 | 109 | return $propertyValues[\count($propertyValues) - 1][self::VALUE]; 110 | } 111 | 112 | public function setValue(object|array &$objectOrArray, string|PropertyPathInterface $propertyPath, mixed $value): void 113 | { 114 | if (\is_object($objectOrArray) && (false === strpbrk((string) $propertyPath, '.[') || $objectOrArray instanceof \stdClass && property_exists($objectOrArray, $propertyPath))) { 115 | $zval = [ 116 | self::VALUE => $objectOrArray, 117 | ]; 118 | 119 | try { 120 | $this->writeProperty($zval, $propertyPath, $value); 121 | 122 | return; 123 | } catch (\TypeError $e) { 124 | self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0, $propertyPath, $e); 125 | // It wasn't thrown in this class so rethrow it 126 | throw $e; 127 | } 128 | } 129 | 130 | $propertyPath = $this->getPropertyPath($propertyPath); 131 | 132 | $zval = [ 133 | self::VALUE => $objectOrArray, 134 | self::REF => &$objectOrArray, 135 | ]; 136 | $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1); 137 | $overwrite = true; 138 | 139 | try { 140 | for ($i = \count($propertyValues) - 1; 0 <= $i; --$i) { 141 | $zval = $propertyValues[$i]; 142 | unset($propertyValues[$i]); 143 | 144 | // You only need set value for current element if: 145 | // 1. it's the parent of the last index element 146 | // OR 147 | // 2. its child is not passed by reference 148 | // 149 | // This may avoid unnecessary value setting process for array elements. 150 | // For example: 151 | // '[a][b][c]' => 'old-value' 152 | // If you want to change its value to 'new-value', 153 | // you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]' 154 | if ($overwrite) { 155 | $property = $propertyPath->getElement($i); 156 | 157 | if ($propertyPath->isIndex($i)) { 158 | if ($overwrite = !isset($zval[self::REF])) { 159 | $ref = &$zval[self::REF]; 160 | $ref = $zval[self::VALUE]; 161 | } 162 | $this->writeIndex($zval, $property, $value); 163 | if ($overwrite) { 164 | $zval[self::VALUE] = $zval[self::REF]; 165 | } 166 | } else { 167 | $this->writeProperty($zval, $property, $value); 168 | } 169 | 170 | // if current element is an object 171 | // OR 172 | // if current element's reference chain is not broken - current element 173 | // as well as all its ancients in the property path are all passed by reference, 174 | // then there is no need to continue the value setting process 175 | if (\is_object($zval[self::VALUE]) || isset($zval[self::IS_REF_CHAINED])) { 176 | break; 177 | } 178 | } 179 | 180 | $value = $zval[self::VALUE]; 181 | } 182 | } catch (\TypeError $e) { 183 | self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0, $propertyPath, $e); 184 | 185 | // It wasn't thrown in this class so rethrow it 186 | throw $e; 187 | } 188 | } 189 | 190 | private static function throwInvalidArgumentException(string $message, array $trace, int $i, string $propertyPath, ?\Throwable $previous = null): void 191 | { 192 | if (!isset($trace[$i]['file']) || __FILE__ !== $trace[$i]['file']) { 193 | return; 194 | } 195 | if (preg_match('/^\S+::\S+\(\): Argument #\d+ \(\$\S+\) must be of type (\S+), (\S+) given/', $message, $matches)) { 196 | [, $expectedType, $actualType] = $matches; 197 | 198 | throw new InvalidTypeException($expectedType, $actualType, $propertyPath, $previous); 199 | } 200 | if (preg_match('/^Cannot assign (\S+) to property \S+::\$\S+ of type (\S+)$/', $message, $matches)) { 201 | [, $actualType, $expectedType] = $matches; 202 | 203 | throw new InvalidTypeException($expectedType, $actualType, $propertyPath, $previous); 204 | } 205 | } 206 | 207 | public function isReadable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool 208 | { 209 | if (!$propertyPath instanceof PropertyPathInterface) { 210 | $propertyPath = new PropertyPath($propertyPath); 211 | } 212 | 213 | try { 214 | $zval = [ 215 | self::VALUE => $objectOrArray, 216 | ]; 217 | 218 | // handle stdClass with properties with a dot in the name 219 | if ($objectOrArray instanceof \stdClass && str_contains($propertyPath, '.') && property_exists($objectOrArray, $propertyPath)) { 220 | $this->readProperty($zval, $propertyPath, $this->ignoreInvalidProperty); 221 | } else { 222 | $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); 223 | } 224 | 225 | return true; 226 | } catch (AccessException|UnexpectedTypeException) { 227 | return false; 228 | } 229 | } 230 | 231 | public function isWritable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool 232 | { 233 | $propertyPath = $this->getPropertyPath($propertyPath); 234 | 235 | try { 236 | $zval = [ 237 | self::VALUE => $objectOrArray, 238 | ]; 239 | 240 | // handle stdClass with properties with a dot in the name 241 | if ($objectOrArray instanceof \stdClass && str_contains($propertyPath, '.') && property_exists($objectOrArray, $propertyPath)) { 242 | $this->readProperty($zval, $propertyPath, $this->ignoreInvalidProperty); 243 | 244 | return true; 245 | } 246 | 247 | $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1); 248 | 249 | for ($i = \count($propertyValues) - 1; 0 <= $i; --$i) { 250 | $zval = $propertyValues[$i]; 251 | unset($propertyValues[$i]); 252 | 253 | if ($propertyPath->isIndex($i)) { 254 | if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) { 255 | return false; 256 | } 257 | } elseif (!\is_object($zval[self::VALUE]) || !$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) { 258 | return false; 259 | } 260 | 261 | if (\is_object($zval[self::VALUE])) { 262 | return true; 263 | } 264 | } 265 | 266 | return true; 267 | } catch (AccessException|UnexpectedTypeException) { 268 | return false; 269 | } 270 | } 271 | 272 | /** 273 | * Reads the path from an object up to a given path index. 274 | * 275 | * @throws UnexpectedTypeException if a value within the path is neither object nor array 276 | * @throws NoSuchIndexException If a non-existing index is accessed 277 | */ 278 | private function readPropertiesUntil(array $zval, PropertyPathInterface $propertyPath, int $lastIndex, bool $ignoreInvalidIndices = true): array 279 | { 280 | if (!\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) { 281 | throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, 0); 282 | } 283 | 284 | // Add the root object to the list 285 | $propertyValues = [$zval]; 286 | 287 | for ($i = 0; $i < $lastIndex; ++$i) { 288 | $property = $propertyPath->getElement($i); 289 | $isIndex = $propertyPath->isIndex($i); 290 | $isNullSafe = $propertyPath->isNullSafe($i); 291 | 292 | if ($isIndex) { 293 | // Create missing nested arrays on demand 294 | if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) 295 | || (\is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !\array_key_exists($property, $zval[self::VALUE])) 296 | ) { 297 | if (!$ignoreInvalidIndices && !$isNullSafe) { 298 | if (!\is_array($zval[self::VALUE])) { 299 | if (!$zval[self::VALUE] instanceof \Traversable) { 300 | throw new NoSuchIndexException(\sprintf('Cannot read index "%s" while trying to traverse path "%s".', $property, (string) $propertyPath)); 301 | } 302 | 303 | $zval[self::VALUE] = iterator_to_array($zval[self::VALUE]); 304 | } 305 | 306 | throw new NoSuchIndexException(\sprintf('Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".', $property, (string) $propertyPath, print_r(array_keys($zval[self::VALUE]), true))); 307 | } 308 | 309 | if ($i + 1 < $propertyPath->getLength()) { 310 | if (isset($zval[self::REF])) { 311 | $zval[self::VALUE][$property] = []; 312 | $zval[self::REF] = $zval[self::VALUE]; 313 | } else { 314 | $zval[self::VALUE] = [$property => []]; 315 | } 316 | } 317 | } 318 | 319 | $zval = $this->readIndex($zval, $property); 320 | } elseif ($isNullSafe && !\is_object($zval[self::VALUE])) { 321 | $zval[self::VALUE] = null; 322 | } else { 323 | $zval = $this->readProperty($zval, $property, $this->ignoreInvalidProperty, $isNullSafe); 324 | } 325 | 326 | // the final value of the path must not be validated 327 | if ($i + 1 < $propertyPath->getLength() && !\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE]) && !$isNullSafe) { 328 | throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1); 329 | } 330 | 331 | if (isset($zval[self::REF]) && (0 === $i || isset($propertyValues[$i - 1][self::IS_REF_CHAINED]))) { 332 | // Set the IS_REF_CHAINED flag to true if: 333 | // current property is passed by reference and 334 | // it is the first element in the property path or 335 | // the IS_REF_CHAINED flag of its parent element is true 336 | // Basically, this flag is true only when the reference chain from the top element to current element is not broken 337 | $zval[self::IS_REF_CHAINED] = true; 338 | } 339 | 340 | $propertyValues[] = $zval; 341 | 342 | if ($isNullSafe && null === $zval[self::VALUE]) { 343 | break; 344 | } 345 | } 346 | 347 | return $propertyValues; 348 | } 349 | 350 | /** 351 | * Reads a key from an array-like structure. 352 | * 353 | * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array 354 | */ 355 | private function readIndex(array $zval, string|int $index): array 356 | { 357 | if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) { 358 | throw new NoSuchIndexException(\sprintf('Cannot read index "%s" from object of type "%s" because it doesn\'t implement \ArrayAccess.', $index, get_debug_type($zval[self::VALUE]))); 359 | } 360 | 361 | $result = self::RESULT_PROTO; 362 | 363 | if (isset($zval[self::VALUE][$index])) { 364 | $result[self::VALUE] = $zval[self::VALUE][$index]; 365 | 366 | if (!isset($zval[self::REF])) { 367 | // Save creating references when doing read-only lookups 368 | } elseif (\is_array($zval[self::VALUE])) { 369 | $result[self::REF] = &$zval[self::REF][$index]; 370 | } elseif (\is_object($result[self::VALUE])) { 371 | $result[self::REF] = $result[self::VALUE]; 372 | } 373 | } 374 | 375 | return $result; 376 | } 377 | 378 | /** 379 | * Reads the value of a property from an object. 380 | * 381 | * @throws NoSuchPropertyException If $ignoreInvalidProperty is false and the property does not exist or is not public 382 | */ 383 | private function readProperty(array $zval, string $property, bool $ignoreInvalidProperty = false, bool $isNullSafe = false): array 384 | { 385 | if (!\is_object($zval[self::VALUE])) { 386 | throw new NoSuchPropertyException(\sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.', $property)); 387 | } 388 | 389 | $result = self::RESULT_PROTO; 390 | $object = $zval[self::VALUE]; 391 | $class = $object::class; 392 | $access = $this->getReadInfo($class, $property); 393 | 394 | if (null !== $access) { 395 | $name = $access->getName(); 396 | $type = $access->getType(); 397 | 398 | try { 399 | if (PropertyReadInfo::TYPE_METHOD === $type) { 400 | try { 401 | $result[self::VALUE] = $object->$name(); 402 | } catch (\TypeError $e) { 403 | [$trace] = $e->getTrace(); 404 | 405 | // handle uninitialized properties in PHP >= 7 406 | if (__FILE__ === ($trace['file'] ?? null) 407 | && $name === $trace['function'] 408 | && $object instanceof $trace['class'] 409 | && preg_match('/Return value (?:of .*::\w+\(\) )?must be of (?:the )?type (\w+), null returned$/', $e->getMessage(), $matches) 410 | ) { 411 | throw new UninitializedPropertyException(\sprintf('The method "%s::%s()" returned "null", but expected type "%3$s". Did you forget to initialize a property or to make the return type nullable using "?%3$s"?', get_debug_type($object), $name, $matches[1]), 0, $e); 412 | } 413 | 414 | throw $e; 415 | } 416 | } elseif (PropertyReadInfo::TYPE_PROPERTY === $type) { 417 | if (!isset($object->$name) && !\array_key_exists($name, (array) $object)) { 418 | try { 419 | $r = new \ReflectionProperty($class, $name); 420 | 421 | if ($r->isPublic() && !$r->hasType()) { 422 | throw new UninitializedPropertyException(\sprintf('The property "%s::$%s" is not initialized.', $class, $name)); 423 | } 424 | } catch (\ReflectionException $e) { 425 | if (!$ignoreInvalidProperty) { 426 | throw new NoSuchPropertyException(\sprintf('Can\'t get a way to read the property "%s" in class "%s".', $property, $class)); 427 | } 428 | } 429 | } 430 | 431 | $result[self::VALUE] = $object->$name; 432 | 433 | if (isset($zval[self::REF]) && $access->canBeReference()) { 434 | $result[self::REF] = &$object->$name; 435 | } 436 | } 437 | } catch (\Error $e) { 438 | // handle uninitialized properties in PHP >= 7.4 439 | if (preg_match('/^Typed property ([\w\\\\@]+)::\$(\w+) must not be accessed before initialization$/', $e->getMessage(), $matches) || preg_match('/^Cannot access uninitialized non-nullable property ([\w\\\\@]+)::\$(\w+) by reference$/', $e->getMessage(), $matches)) { 440 | $r = new \ReflectionProperty(str_contains($matches[1], '@anonymous') ? $class : $matches[1], $matches[2]); 441 | $type = ($type = $r->getType()) instanceof \ReflectionNamedType ? $type->getName() : (string) $type; 442 | 443 | throw new UninitializedPropertyException(\sprintf('The property "%s::$%s" is not readable because it is typed "%s". You should initialize it or declare a default value instead.', $matches[1], $r->getName(), $type), 0, $e); 444 | } 445 | 446 | throw $e; 447 | } 448 | } elseif (property_exists($object, $property) && \array_key_exists($property, (array) $object)) { 449 | $result[self::VALUE] = $object->$property; 450 | if (isset($zval[self::REF])) { 451 | $result[self::REF] = &$object->$property; 452 | } 453 | } elseif ($isNullSafe) { 454 | $result[self::VALUE] = null; 455 | } elseif (!$ignoreInvalidProperty) { 456 | throw new NoSuchPropertyException(\sprintf('Can\'t get a way to read the property "%s" in class "%s".', $property, $class)); 457 | } 458 | 459 | // Objects are always passed around by reference 460 | if (isset($zval[self::REF]) && \is_object($result[self::VALUE])) { 461 | $result[self::REF] = $result[self::VALUE]; 462 | } 463 | 464 | return $result; 465 | } 466 | 467 | /** 468 | * Guesses how to read the property value. 469 | */ 470 | private function getReadInfo(string $class, string $property): ?PropertyReadInfo 471 | { 472 | $key = str_replace('\\', '.', $class).'..'.$property; 473 | 474 | if (isset($this->readPropertyCache[$key])) { 475 | return $this->readPropertyCache[$key]; 476 | } 477 | 478 | if ($this->cacheItemPool) { 479 | $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.rawurlencode($key)); 480 | if ($item->isHit()) { 481 | return $this->readPropertyCache[$key] = $item->get(); 482 | } 483 | } 484 | 485 | $accessor = $this->readInfoExtractor->getReadInfo($class, $property, [ 486 | 'enable_getter_setter_extraction' => true, 487 | 'enable_magic_methods_extraction' => $this->magicMethodsFlags, 488 | 'enable_constructor_extraction' => false, 489 | ]); 490 | 491 | if (isset($item)) { 492 | $this->cacheItemPool->save($item->set($accessor)); 493 | } 494 | 495 | return $this->readPropertyCache[$key] = $accessor; 496 | } 497 | 498 | /** 499 | * Sets the value of an index in a given array-accessible value. 500 | * 501 | * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array 502 | */ 503 | private function writeIndex(array $zval, string|int $index, mixed $value): void 504 | { 505 | if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) { 506 | throw new NoSuchIndexException(\sprintf('Cannot modify index "%s" in object of type "%s" because it doesn\'t implement \ArrayAccess.', $index, get_debug_type($zval[self::VALUE]))); 507 | } 508 | 509 | $zval[self::REF][$index] = $value; 510 | } 511 | 512 | /** 513 | * Sets the value of a property in the given object. 514 | * 515 | * @throws NoSuchPropertyException if the property does not exist or is not public 516 | */ 517 | private function writeProperty(array $zval, string $property, mixed $value, bool $recursive = false): void 518 | { 519 | if (!\is_object($zval[self::VALUE])) { 520 | throw new NoSuchPropertyException(\sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%1$s]" instead?', $property)); 521 | } 522 | 523 | $object = $zval[self::VALUE]; 524 | $class = $object::class; 525 | $mutator = $this->getWriteInfo($class, $property, $value); 526 | 527 | try { 528 | if (PropertyWriteInfo::TYPE_NONE !== $mutator->getType()) { 529 | $type = $mutator->getType(); 530 | 531 | if (PropertyWriteInfo::TYPE_METHOD === $type) { 532 | $object->{$mutator->getName()}($value); 533 | } elseif (PropertyWriteInfo::TYPE_PROPERTY === $type) { 534 | $object->{$mutator->getName()} = $value; 535 | } elseif (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $type) { 536 | $this->writeCollection($zval, $property, $value, $mutator->getAdderInfo(), $mutator->getRemoverInfo()); 537 | } 538 | } elseif ($object instanceof \stdClass && property_exists($object, $property)) { 539 | $object->$property = $value; 540 | } elseif (!$this->ignoreInvalidProperty) { 541 | if ($mutator->hasErrors()) { 542 | throw new NoSuchPropertyException(implode('. ', $mutator->getErrors()).'.'); 543 | } 544 | 545 | throw new NoSuchPropertyException(\sprintf('Could not determine access type for property "%s" in class "%s".', $property, get_debug_type($object))); 546 | } 547 | } catch (\TypeError $e) { 548 | if ($recursive || !$value instanceof \DateTimeInterface || !\in_array($value::class, ['DateTime', 'DateTimeImmutable'], true) || __FILE__ !== ($e->getTrace()[0]['file'] ?? null)) { 549 | throw $e; 550 | } 551 | 552 | $value = $value instanceof \DateTimeImmutable ? \DateTime::createFromImmutable($value) : \DateTimeImmutable::createFromMutable($value); 553 | try { 554 | $this->writeProperty($zval, $property, $value, true); 555 | } catch (\TypeError) { 556 | throw $e; // throw the previous error 557 | } 558 | } 559 | } 560 | 561 | /** 562 | * Adjusts a collection-valued property by calling add*() and remove*() methods. 563 | */ 564 | private function writeCollection(array $zval, string $property, iterable $collection, PropertyWriteInfo $addMethod, PropertyWriteInfo $removeMethod): void 565 | { 566 | // At this point the add and remove methods have been found 567 | $previousValue = $this->readProperty($zval, $property); 568 | $previousValue = $previousValue[self::VALUE]; 569 | 570 | $removeMethodName = $removeMethod->getName(); 571 | $addMethodName = $addMethod->getName(); 572 | 573 | if ($previousValue instanceof \Traversable) { 574 | $previousValue = iterator_to_array($previousValue); 575 | } 576 | if ($previousValue && \is_array($previousValue)) { 577 | if (\is_object($collection)) { 578 | $collection = iterator_to_array($collection); 579 | } 580 | foreach ($previousValue as $key => $item) { 581 | if (!\in_array($item, $collection, true)) { 582 | unset($previousValue[$key]); 583 | $zval[self::VALUE]->$removeMethodName($item); 584 | } 585 | } 586 | } else { 587 | $previousValue = false; 588 | } 589 | 590 | foreach ($collection as $item) { 591 | if (!$previousValue || !\in_array($item, $previousValue, true)) { 592 | $zval[self::VALUE]->$addMethodName($item); 593 | } 594 | } 595 | } 596 | 597 | private function getWriteInfo(string $class, string $property, mixed $value): PropertyWriteInfo 598 | { 599 | $useAdderAndRemover = is_iterable($value); 600 | $key = str_replace('\\', '.', $class).'..'.$property.'..'.(int) $useAdderAndRemover; 601 | 602 | if (isset($this->writePropertyCache[$key])) { 603 | return $this->writePropertyCache[$key]; 604 | } 605 | 606 | if ($this->cacheItemPool) { 607 | $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.rawurlencode($key)); 608 | if ($item->isHit()) { 609 | return $this->writePropertyCache[$key] = $item->get(); 610 | } 611 | } 612 | 613 | $mutator = $this->writeInfoExtractor->getWriteInfo($class, $property, [ 614 | 'enable_getter_setter_extraction' => true, 615 | 'enable_magic_methods_extraction' => $this->magicMethodsFlags, 616 | 'enable_constructor_extraction' => false, 617 | 'enable_adder_remover_extraction' => $useAdderAndRemover, 618 | ]); 619 | 620 | if (isset($item)) { 621 | $this->cacheItemPool->save($item->set($mutator)); 622 | } 623 | 624 | return $this->writePropertyCache[$key] = $mutator; 625 | } 626 | 627 | /** 628 | * Returns whether a property is writable in the given object. 629 | */ 630 | private function isPropertyWritable(object $object, string $property): bool 631 | { 632 | if ($object instanceof \stdClass && property_exists($object, $property)) { 633 | return true; 634 | } 635 | 636 | $mutatorForArray = $this->getWriteInfo($object::class, $property, []); 637 | if (PropertyWriteInfo::TYPE_PROPERTY === $mutatorForArray->getType()) { 638 | return 'public' === $mutatorForArray->getVisibility(); 639 | } 640 | 641 | if (PropertyWriteInfo::TYPE_NONE !== $mutatorForArray->getType()) { 642 | return true; 643 | } 644 | 645 | $mutator = $this->getWriteInfo($object::class, $property, ''); 646 | 647 | return PropertyWriteInfo::TYPE_NONE !== $mutator->getType(); 648 | } 649 | 650 | /** 651 | * Gets a PropertyPath instance and caches it. 652 | */ 653 | private function getPropertyPath(string|PropertyPath $propertyPath): PropertyPath 654 | { 655 | if ($propertyPath instanceof PropertyPathInterface) { 656 | // Don't call the copy constructor has it is not needed here 657 | return $propertyPath; 658 | } 659 | 660 | if (isset($this->propertyPathCache[$propertyPath])) { 661 | return $this->propertyPathCache[$propertyPath]; 662 | } 663 | 664 | if ($this->cacheItemPool) { 665 | $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.rawurlencode($propertyPath)); 666 | if ($item->isHit()) { 667 | return $this->propertyPathCache[$propertyPath] = $item->get(); 668 | } 669 | } 670 | 671 | $propertyPathInstance = new PropertyPath($propertyPath); 672 | if (isset($item)) { 673 | $item->set($propertyPathInstance); 674 | $this->cacheItemPool->save($item); 675 | } 676 | 677 | return $this->propertyPathCache[$propertyPath] = $propertyPathInstance; 678 | } 679 | 680 | /** 681 | * Creates the APCu adapter if applicable. 682 | * 683 | * @throws \LogicException When the Cache Component isn't available 684 | */ 685 | public static function createCache(string $namespace, int $defaultLifetime, string $version, ?LoggerInterface $logger = null): AdapterInterface 686 | { 687 | if (!class_exists(ApcuAdapter::class)) { 688 | throw new \LogicException(\sprintf('The Symfony Cache component must be installed to use "%s()".', __METHOD__)); 689 | } 690 | 691 | if (!ApcuAdapter::isSupported()) { 692 | return new NullAdapter(); 693 | } 694 | 695 | $apcu = new ApcuAdapter($namespace, $defaultLifetime / 5, $version); 696 | if ('cli' === \PHP_SAPI && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) { 697 | $apcu->setLogger(new NullLogger()); 698 | } elseif (null !== $logger) { 699 | $apcu->setLogger($logger); 700 | } 701 | 702 | return $apcu; 703 | } 704 | } 705 | --------------------------------------------------------------------------------