vendor/symfony/property-access/PropertyAccessor.php line 114

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\PropertyAccess;
  11. use Psr\Cache\CacheItemPoolInterface;
  12. use Psr\Log\LoggerInterface;
  13. use Psr\Log\NullLogger;
  14. use Symfony\Component\Cache\Adapter\AdapterInterface;
  15. use Symfony\Component\Cache\Adapter\ApcuAdapter;
  16. use Symfony\Component\Cache\Adapter\NullAdapter;
  17. use Symfony\Component\Inflector\Inflector;
  18. use Symfony\Component\PropertyAccess\Exception\AccessException;
  19. use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
  20. use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
  21. use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
  22. use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
  23. /**
  24.  * Default implementation of {@link PropertyAccessorInterface}.
  25.  *
  26.  * @author Bernhard Schussek <bschussek@gmail.com>
  27.  * @author Kévin Dunglas <dunglas@gmail.com>
  28.  * @author Nicolas Grekas <p@tchwork.com>
  29.  */
  30. class PropertyAccessor implements PropertyAccessorInterface
  31. {
  32.     private const VALUE 0;
  33.     private const REF 1;
  34.     private const IS_REF_CHAINED 2;
  35.     private const ACCESS_HAS_PROPERTY 0;
  36.     private const ACCESS_TYPE 1;
  37.     private const ACCESS_NAME 2;
  38.     private const ACCESS_REF 3;
  39.     private const ACCESS_ADDER 4;
  40.     private const ACCESS_REMOVER 5;
  41.     private const ACCESS_TYPE_METHOD 0;
  42.     private const ACCESS_TYPE_PROPERTY 1;
  43.     private const ACCESS_TYPE_MAGIC 2;
  44.     private const ACCESS_TYPE_ADDER_AND_REMOVER 3;
  45.     private const ACCESS_TYPE_NOT_FOUND 4;
  46.     private const CACHE_PREFIX_READ 'r';
  47.     private const CACHE_PREFIX_WRITE 'w';
  48.     private const CACHE_PREFIX_PROPERTY_PATH 'p';
  49.     /**
  50.      * @var bool
  51.      */
  52.     private $magicCall;
  53.     private $ignoreInvalidIndices;
  54.     private $ignoreInvalidProperty;
  55.     /**
  56.      * @var CacheItemPoolInterface
  57.      */
  58.     private $cacheItemPool;
  59.     private $propertyPathCache = [];
  60.     private $readPropertyCache = [];
  61.     private $writePropertyCache = [];
  62.     private const RESULT_PROTO = [self::VALUE => null];
  63.     /**
  64.      * Should not be used by application code. Use
  65.      * {@link PropertyAccess::createPropertyAccessor()} instead.
  66.      */
  67.     public function __construct(bool $magicCall falsebool $throwExceptionOnInvalidIndex falseCacheItemPoolInterface $cacheItemPool nullbool $throwExceptionOnInvalidPropertyPath true)
  68.     {
  69.         $this->magicCall $magicCall;
  70.         $this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex;
  71.         $this->cacheItemPool $cacheItemPool instanceof NullAdapter null $cacheItemPool// Replace the NullAdapter by the null value
  72.         $this->ignoreInvalidProperty = !$throwExceptionOnInvalidPropertyPath;
  73.     }
  74.     /**
  75.      * {@inheritdoc}
  76.      */
  77.     public function getValue($objectOrArray$propertyPath)
  78.     {
  79.         $zval = [
  80.             self::VALUE => $objectOrArray,
  81.         ];
  82.         if (\is_object($objectOrArray) && false === strpbrk((string) $propertyPath'.[')) {
  83.             return $this->readProperty($zval$propertyPath$this->ignoreInvalidProperty)[self::VALUE];
  84.         }
  85.         $propertyPath $this->getPropertyPath($propertyPath);
  86.         $propertyValues $this->readPropertiesUntil($zval$propertyPath$propertyPath->getLength(), $this->ignoreInvalidIndices);
  87.         return $propertyValues[\count($propertyValues) - 1][self::VALUE];
  88.     }
  89.     /**
  90.      * {@inheritdoc}
  91.      */
  92.     public function setValue(&$objectOrArray$propertyPath$value)
  93.     {
  94.         if (\is_object($objectOrArray) && false === strpbrk((string) $propertyPath'.[')) {
  95.             $zval = [
  96.                 self::VALUE => $objectOrArray,
  97.             ];
  98.             try {
  99.                 $this->writeProperty($zval$propertyPath$value);
  100.                 return;
  101.             } catch (\TypeError $e) {
  102.                 self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0$propertyPath$e);
  103.                 // It wasn't thrown in this class so rethrow it
  104.                 throw $e;
  105.             }
  106.         }
  107.         $propertyPath $this->getPropertyPath($propertyPath);
  108.         $zval = [
  109.             self::VALUE => $objectOrArray,
  110.             self::REF => &$objectOrArray,
  111.         ];
  112.         $propertyValues $this->readPropertiesUntil($zval$propertyPath$propertyPath->getLength() - 1);
  113.         $overwrite true;
  114.         try {
  115.             for ($i = \count($propertyValues) - 1<= $i; --$i) {
  116.                 $zval $propertyValues[$i];
  117.                 unset($propertyValues[$i]);
  118.                 // You only need set value for current element if:
  119.                 // 1. it's the parent of the last index element
  120.                 // OR
  121.                 // 2. its child is not passed by reference
  122.                 //
  123.                 // This may avoid uncessary value setting process for array elements.
  124.                 // For example:
  125.                 // '[a][b][c]' => 'old-value'
  126.                 // If you want to change its value to 'new-value',
  127.                 // you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]'
  128.                 if ($overwrite) {
  129.                     $property $propertyPath->getElement($i);
  130.                     if ($propertyPath->isIndex($i)) {
  131.                         if ($overwrite = !isset($zval[self::REF])) {
  132.                             $ref = &$zval[self::REF];
  133.                             $ref $zval[self::VALUE];
  134.                         }
  135.                         $this->writeIndex($zval$property$value);
  136.                         if ($overwrite) {
  137.                             $zval[self::VALUE] = $zval[self::REF];
  138.                         }
  139.                     } else {
  140.                         $this->writeProperty($zval$property$value);
  141.                     }
  142.                     // if current element is an object
  143.                     // OR
  144.                     // if current element's reference chain is not broken - current element
  145.                     // as well as all its ancients in the property path are all passed by reference,
  146.                     // then there is no need to continue the value setting process
  147.                     if (\is_object($zval[self::VALUE]) || isset($zval[self::IS_REF_CHAINED])) {
  148.                         break;
  149.                     }
  150.                 }
  151.                 $value $zval[self::VALUE];
  152.             }
  153.         } catch (\TypeError $e) {
  154.             self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0$propertyPath$e);
  155.             // It wasn't thrown in this class so rethrow it
  156.             throw $e;
  157.         }
  158.     }
  159.     private static function throwInvalidArgumentException(string $message, array $traceint $istring $propertyPath, \Throwable $previous null): void
  160.     {
  161.         if (!isset($trace[$i]['file']) || __FILE__ !== $trace[$i]['file']) {
  162.             return;
  163.         }
  164.         if (\PHP_VERSION_ID 80000) {
  165.             if (preg_match('/^Typed property \S+::\$\S+ must be (\S+), (\S+) used$/'$message$matches)) {
  166.                 [, $expectedType$actualType] = $matches;
  167.                 throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".'$expectedType'NULL' === $actualType 'null' $actualType$propertyPath), 0$previous);
  168.             }
  169.             if (!str_starts_with($message'Argument ')) {
  170.                 return;
  171.             }
  172.             $pos strpos($message$delim 'must be of the type ') ?: (strpos($message$delim 'must be an instance of ') ?: strpos($message$delim 'must implement interface '));
  173.             $pos += \strlen($delim);
  174.             $j strpos($message','$pos);
  175.             $type substr($message$jstrpos($message' given'$j) - $j 2);
  176.             $message substr($message$pos$j $pos);
  177.             throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".'$message'NULL' === $type 'null' $type$propertyPath), 0$previous);
  178.         }
  179.         if (preg_match('/^\S+::\S+\(\): Argument #\d+ \(\$\S+\) must be of type (\S+), (\S+) given/'$message$matches)) {
  180.             [, $expectedType$actualType] = $matches;
  181.             throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".'$expectedType'NULL' === $actualType 'null' $actualType$propertyPath), 0$previous);
  182.         }
  183.         if (preg_match('/^Cannot assign (\S+) to property \S+::\$\S+ of type (\S+)$/'$message$matches)) {
  184.             [, $actualType$expectedType] = $matches;
  185.             throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".'$expectedType'NULL' === $actualType 'null' $actualType$propertyPath), 0$previous);
  186.         }
  187.     }
  188.     /**
  189.      * {@inheritdoc}
  190.      */
  191.     public function isReadable($objectOrArray$propertyPath)
  192.     {
  193.         if (!$propertyPath instanceof PropertyPathInterface) {
  194.             $propertyPath = new PropertyPath($propertyPath);
  195.         }
  196.         try {
  197.             $zval = [
  198.                 self::VALUE => $objectOrArray,
  199.             ];
  200.             $this->readPropertiesUntil($zval$propertyPath$propertyPath->getLength(), $this->ignoreInvalidIndices);
  201.             return true;
  202.         } catch (AccessException $e) {
  203.             return false;
  204.         } catch (UnexpectedTypeException $e) {
  205.             return false;
  206.         }
  207.     }
  208.     /**
  209.      * {@inheritdoc}
  210.      */
  211.     public function isWritable($objectOrArray$propertyPath)
  212.     {
  213.         $propertyPath $this->getPropertyPath($propertyPath);
  214.         try {
  215.             $zval = [
  216.                 self::VALUE => $objectOrArray,
  217.             ];
  218.             $propertyValues $this->readPropertiesUntil($zval$propertyPath$propertyPath->getLength() - 1);
  219.             for ($i = \count($propertyValues) - 1<= $i; --$i) {
  220.                 $zval $propertyValues[$i];
  221.                 unset($propertyValues[$i]);
  222.                 if ($propertyPath->isIndex($i)) {
  223.                     if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
  224.                         return false;
  225.                     }
  226.                 } else {
  227.                     if (!$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) {
  228.                         return false;
  229.                     }
  230.                 }
  231.                 if (\is_object($zval[self::VALUE])) {
  232.                     return true;
  233.                 }
  234.             }
  235.             return true;
  236.         } catch (AccessException $e) {
  237.             return false;
  238.         } catch (UnexpectedTypeException $e) {
  239.             return false;
  240.         }
  241.     }
  242.     /**
  243.      * Reads the path from an object up to a given path index.
  244.      *
  245.      * @throws UnexpectedTypeException if a value within the path is neither object nor array
  246.      * @throws NoSuchIndexException    If a non-existing index is accessed
  247.      */
  248.     private function readPropertiesUntil(array $zvalPropertyPathInterface $propertyPathint $lastIndexbool $ignoreInvalidIndices true): array
  249.     {
  250.         if (!\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) {
  251.             throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath0);
  252.         }
  253.         // Add the root object to the list
  254.         $propertyValues = [$zval];
  255.         for ($i 0$i $lastIndex; ++$i) {
  256.             $property $propertyPath->getElement($i);
  257.             $isIndex $propertyPath->isIndex($i);
  258.             if ($isIndex) {
  259.                 // Create missing nested arrays on demand
  260.                 if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
  261.                     (\is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !\array_key_exists($property$zval[self::VALUE]))
  262.                 ) {
  263.                     if (!$ignoreInvalidIndices) {
  264.                         if (!\is_array($zval[self::VALUE])) {
  265.                             if (!$zval[self::VALUE] instanceof \Traversable) {
  266.                                 throw new NoSuchIndexException(sprintf('Cannot read index "%s" while trying to traverse path "%s".'$property, (string) $propertyPath));
  267.                             }
  268.                             $zval[self::VALUE] = iterator_to_array($zval[self::VALUE]);
  269.                         }
  270.                         throw new NoSuchIndexException(sprintf('Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".'$property, (string) $propertyPathprint_r(array_keys($zval[self::VALUE]), true)));
  271.                     }
  272.                     if ($i $propertyPath->getLength()) {
  273.                         if (isset($zval[self::REF])) {
  274.                             $zval[self::VALUE][$property] = [];
  275.                             $zval[self::REF] = $zval[self::VALUE];
  276.                         } else {
  277.                             $zval[self::VALUE] = [$property => []];
  278.                         }
  279.                     }
  280.                 }
  281.                 $zval $this->readIndex($zval$property);
  282.             } else {
  283.                 $zval $this->readProperty($zval$property$this->ignoreInvalidProperty);
  284.             }
  285.             // the final value of the path must not be validated
  286.             if ($i $propertyPath->getLength() && !\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) {
  287.                 throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath$i 1);
  288.             }
  289.             if (isset($zval[self::REF]) && (=== $i || isset($propertyValues[$i 1][self::IS_REF_CHAINED]))) {
  290.                 // Set the IS_REF_CHAINED flag to true if:
  291.                 // current property is passed by reference and
  292.                 // it is the first element in the property path or
  293.                 // the IS_REF_CHAINED flag of its parent element is true
  294.                 // Basically, this flag is true only when the reference chain from the top element to current element is not broken
  295.                 $zval[self::IS_REF_CHAINED] = true;
  296.             }
  297.             $propertyValues[] = $zval;
  298.         }
  299.         return $propertyValues;
  300.     }
  301.     /**
  302.      * Reads a key from an array-like structure.
  303.      *
  304.      * @param string|int $index The key to read
  305.      *
  306.      * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
  307.      */
  308.     private function readIndex(array $zval$index): array
  309.     {
  310.         if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
  311.             throw new NoSuchIndexException(sprintf('Cannot read index "%s" from object of type "%s" because it doesn\'t implement \ArrayAccess.'$index, \get_class($zval[self::VALUE])));
  312.         }
  313.         $result self::RESULT_PROTO;
  314.         if (isset($zval[self::VALUE][$index])) {
  315.             $result[self::VALUE] = $zval[self::VALUE][$index];
  316.             if (!isset($zval[self::REF])) {
  317.                 // Save creating references when doing read-only lookups
  318.             } elseif (\is_array($zval[self::VALUE])) {
  319.                 $result[self::REF] = &$zval[self::REF][$index];
  320.             } elseif (\is_object($result[self::VALUE])) {
  321.                 $result[self::REF] = $result[self::VALUE];
  322.             }
  323.         }
  324.         return $result;
  325.     }
  326.     /**
  327.      * Reads the value of a property from an object.
  328.      *
  329.      * @throws NoSuchPropertyException If $ignoreInvalidProperty is false and the property does not exist or is not public
  330.      */
  331.     private function readProperty(array $zvalstring $propertybool $ignoreInvalidProperty false): array
  332.     {
  333.         if (!\is_object($zval[self::VALUE])) {
  334.             throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.'$property));
  335.         }
  336.         $result self::RESULT_PROTO;
  337.         $object $zval[self::VALUE];
  338.         $class = \get_class($object);
  339.         $access $this->getReadAccessInfo(\get_class($object), $property);
  340.         try {
  341.             if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
  342.                 try {
  343.                     $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
  344.                 } catch (\TypeError $e) {
  345.                     [$trace] = $e->getTrace();
  346.                     // handle uninitialized properties in PHP >= 7
  347.                     if (__FILE__ === $trace['file']
  348.                         && $access[self::ACCESS_NAME] === $trace['function']
  349.                         && $object instanceof $trace['class']
  350.                         && preg_match('/Return value (?:of .*::\w+\(\) )?must be of (?:the )?type (\w+), null returned$/'$e->getMessage(), $matches)
  351.                     ) {
  352.                         throw new AccessException(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), $access[self::ACCESS_NAME], $matches[1]), 0$e);
  353.                     }
  354.                     throw $e;
  355.                 }
  356.             } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
  357.                 $name $access[self::ACCESS_NAME];
  358.                 if ($access[self::ACCESS_REF] && !isset($object->$name) && !\array_key_exists($name, (array) $object) && (\PHP_VERSION_ID 70400 || !(new \ReflectionProperty($class$name))->hasType())) {
  359.                     throw new AccessException(sprintf('The property "%s::$%s" is not initialized.'$class$name));
  360.                 }
  361.                 $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
  362.                 if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
  363.                     $result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
  364.                 }
  365.             } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property)) {
  366.                 // Needed to support \stdClass instances. We need to explicitly
  367.                 // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
  368.                 // a *protected* property was found on the class, property_exists()
  369.                 // returns true, consequently the following line will result in a
  370.                 // fatal error.
  371.                 $result[self::VALUE] = $object->$property;
  372.                 if (isset($zval[self::REF])) {
  373.                     $result[self::REF] = &$object->$property;
  374.                 }
  375.             } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
  376.                 // we call the getter and hope the __call do the job
  377.                 $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
  378.             } elseif (!$ignoreInvalidProperty) {
  379.                 throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
  380.             }
  381.         } catch (\Error $e) {
  382.             // handle uninitialized properties in PHP >= 7.4
  383.             if (\PHP_VERSION_ID >= 70400 && preg_match('/^Typed property ([\w\\\\@]+)::\$(\w+) must not be accessed before initialization$/'$e->getMessage(), $matches)) {
  384.                 $r = new \ReflectionProperty(str_contains($matches[1], '@anonymous') ? $class $matches[1], $matches[2]);
  385.                 $type = ($type $r->getType()) instanceof \ReflectionNamedType $type->getName() : (string) $type;
  386.                 throw new AccessException(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);
  387.             }
  388.             throw $e;
  389.         }
  390.         // Objects are always passed around by reference
  391.         if (isset($zval[self::REF]) && \is_object($result[self::VALUE])) {
  392.             $result[self::REF] = $result[self::VALUE];
  393.         }
  394.         return $result;
  395.     }
  396.     /**
  397.      * Guesses how to read the property value.
  398.      */
  399.     private function getReadAccessInfo(string $classstring $property): array
  400.     {
  401.         $key str_replace('\\''.'$class).'..'.$property;
  402.         if (isset($this->readPropertyCache[$key])) {
  403.             return $this->readPropertyCache[$key];
  404.         }
  405.         if ($this->cacheItemPool) {
  406.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.rawurlencode($key));
  407.             if ($item->isHit()) {
  408.                 return $this->readPropertyCache[$key] = $item->get();
  409.             }
  410.         }
  411.         $access = [];
  412.         $reflClass = new \ReflectionClass($class);
  413.         $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
  414.         $camelProp $this->camelize($property);
  415.         $getter 'get'.$camelProp;
  416.         $getsetter lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
  417.         $isser 'is'.$camelProp;
  418.         $hasser 'has'.$camelProp;
  419.         $canAccessor 'can'.$camelProp;
  420.         if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
  421.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  422.             $access[self::ACCESS_NAME] = $getter;
  423.         } elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
  424.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  425.             $access[self::ACCESS_NAME] = $getsetter;
  426.         } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
  427.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  428.             $access[self::ACCESS_NAME] = $isser;
  429.         } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
  430.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  431.             $access[self::ACCESS_NAME] = $hasser;
  432.         } elseif ($reflClass->hasMethod($canAccessor) && $reflClass->getMethod($canAccessor)->isPublic()) {
  433.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  434.             $access[self::ACCESS_NAME] = $canAccessor;
  435.         } elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
  436.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  437.             $access[self::ACCESS_NAME] = $property;
  438.             $access[self::ACCESS_REF] = false;
  439.         } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
  440.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  441.             $access[self::ACCESS_NAME] = $property;
  442.             $access[self::ACCESS_REF] = true;
  443.         } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
  444.             // we call the getter and hope the __call do the job
  445.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
  446.             $access[self::ACCESS_NAME] = $getter;
  447.         } else {
  448.             $methods = [$getter$getsetter$isser$hasser'__get'];
  449.             if ($this->magicCall) {
  450.                 $methods[] = '__call';
  451.             }
  452.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  453.             $access[self::ACCESS_NAME] = sprintf(
  454.                 'Neither the property "%s" nor one of the methods "%s()" '.
  455.                 'exist and have public access in class "%s".',
  456.                 $property,
  457.                 implode('()", "'$methods),
  458.                 $reflClass->name
  459.             );
  460.         }
  461.         if (isset($item)) {
  462.             $this->cacheItemPool->save($item->set($access));
  463.         }
  464.         return $this->readPropertyCache[$key] = $access;
  465.     }
  466.     /**
  467.      * Sets the value of an index in a given array-accessible value.
  468.      *
  469.      * @param string|int $index The index to write at
  470.      * @param mixed      $value The value to write
  471.      *
  472.      * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
  473.      */
  474.     private function writeIndex(array $zval$index$value)
  475.     {
  476.         if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
  477.             throw new NoSuchIndexException(sprintf('Cannot modify index "%s" in object of type "%s" because it doesn\'t implement \ArrayAccess.'$index, \get_class($zval[self::VALUE])));
  478.         }
  479.         $zval[self::REF][$index] = $value;
  480.     }
  481.     /**
  482.      * Sets the value of a property in the given object.
  483.      *
  484.      * @param mixed $value The value to write
  485.      *
  486.      * @throws NoSuchPropertyException if the property does not exist or is not public
  487.      */
  488.     private function writeProperty(array $zvalstring $property$value)
  489.     {
  490.         if (!\is_object($zval[self::VALUE])) {
  491.             throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%1$s]" instead?'$property));
  492.         }
  493.         $object $zval[self::VALUE];
  494.         $access $this->getWriteAccessInfo(\get_class($object), $property$value);
  495.         if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
  496.             $object->{$access[self::ACCESS_NAME]}($value);
  497.         } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
  498.             $object->{$access[self::ACCESS_NAME]} = $value;
  499.         } elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) {
  500.             $this->writeCollection($zval$property$value$access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]);
  501.         } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property)) {
  502.             // Needed to support \stdClass instances. We need to explicitly
  503.             // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
  504.             // a *protected* property was found on the class, property_exists()
  505.             // returns true, consequently the following line will result in a
  506.             // fatal error.
  507.             $object->$property $value;
  508.         } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
  509.             $object->{$access[self::ACCESS_NAME]}($value);
  510.         } elseif (self::ACCESS_TYPE_NOT_FOUND === $access[self::ACCESS_TYPE]) {
  511.             throw new NoSuchPropertyException(sprintf('Could not determine access type for property "%s" in class "%s"%s.'$property, \get_class($object), isset($access[self::ACCESS_NAME]) ? ': '.$access[self::ACCESS_NAME] : '.'));
  512.         } else {
  513.             throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
  514.         }
  515.     }
  516.     /**
  517.      * Adjusts a collection-valued property by calling add*() and remove*() methods.
  518.      */
  519.     private function writeCollection(array $zvalstring $propertyiterable $collectionstring $addMethodstring $removeMethod)
  520.     {
  521.         // At this point the add and remove methods have been found
  522.         $previousValue $this->readProperty($zval$property);
  523.         $previousValue $previousValue[self::VALUE];
  524.         if ($previousValue instanceof \Traversable) {
  525.             $previousValue iterator_to_array($previousValue);
  526.         }
  527.         if ($previousValue && \is_array($previousValue)) {
  528.             if (\is_object($collection)) {
  529.                 $collection iterator_to_array($collection);
  530.             }
  531.             foreach ($previousValue as $key => $item) {
  532.                 if (!\in_array($item$collectiontrue)) {
  533.                     unset($previousValue[$key]);
  534.                     $zval[self::VALUE]->{$removeMethod}($item);
  535.                 }
  536.             }
  537.         } else {
  538.             $previousValue false;
  539.         }
  540.         foreach ($collection as $item) {
  541.             if (!$previousValue || !\in_array($item$previousValuetrue)) {
  542.                 $zval[self::VALUE]->{$addMethod}($item);
  543.             }
  544.         }
  545.     }
  546.     /**
  547.      * Guesses how to write the property value.
  548.      *
  549.      * @param mixed $value
  550.      */
  551.     private function getWriteAccessInfo(string $classstring $property$value): array
  552.     {
  553.         $useAdderAndRemover is_iterable($value);
  554.         $key str_replace('\\''.'$class).'..'.$property.'..'.(int) $useAdderAndRemover;
  555.         if (isset($this->writePropertyCache[$key])) {
  556.             return $this->writePropertyCache[$key];
  557.         }
  558.         if ($this->cacheItemPool) {
  559.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.rawurlencode($key));
  560.             if ($item->isHit()) {
  561.                 return $this->writePropertyCache[$key] = $item->get();
  562.             }
  563.         }
  564.         $access = [];
  565.         $reflClass = new \ReflectionClass($class);
  566.         $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
  567.         $camelized $this->camelize($property);
  568.         $singulars = (array) Inflector::singularize($camelized);
  569.         $errors = [];
  570.         if ($useAdderAndRemover) {
  571.             foreach ($this->findAdderAndRemover($reflClass$singulars) as $methods) {
  572.                 if (=== \count($methods)) {
  573.                     $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
  574.                     $access[self::ACCESS_ADDER] = $methods[self::ACCESS_ADDER];
  575.                     $access[self::ACCESS_REMOVER] = $methods[self::ACCESS_REMOVER];
  576.                     break;
  577.                 }
  578.                 if (isset($methods[self::ACCESS_ADDER])) {
  579.                     $errors[] = sprintf('The add method "%s" in class "%s" was found, but the corresponding remove method "%s" was not found'$methods['methods'][self::ACCESS_ADDER], $reflClass->name$methods['methods'][self::ACCESS_REMOVER]);
  580.                 }
  581.                 if (isset($methods[self::ACCESS_REMOVER])) {
  582.                     $errors[] = sprintf('The remove method "%s" in class "%s" was found, but the corresponding add method "%s" was not found'$methods['methods'][self::ACCESS_REMOVER], $reflClass->name$methods['methods'][self::ACCESS_ADDER]);
  583.                 }
  584.             }
  585.         }
  586.         if (!isset($access[self::ACCESS_TYPE])) {
  587.             $setter 'set'.$camelized;
  588.             $getsetter lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
  589.             if ($this->isMethodAccessible($reflClass$setter1)) {
  590.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  591.                 $access[self::ACCESS_NAME] = $setter;
  592.             } elseif ($this->isMethodAccessible($reflClass$getsetter1)) {
  593.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  594.                 $access[self::ACCESS_NAME] = $getsetter;
  595.             } elseif ($this->isMethodAccessible($reflClass'__set'2)) {
  596.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  597.                 $access[self::ACCESS_NAME] = $property;
  598.             } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
  599.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  600.                 $access[self::ACCESS_NAME] = $property;
  601.             } elseif ($this->magicCall && $this->isMethodAccessible($reflClass'__call'2)) {
  602.                 // we call the getter and hope the __call do the job
  603.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
  604.                 $access[self::ACCESS_NAME] = $setter;
  605.             } else {
  606.                 foreach ($this->findAdderAndRemover($reflClass$singulars) as $methods) {
  607.                     if (=== \count($methods)) {
  608.                         $errors[] = sprintf(
  609.                             'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
  610.                             'the new value must be an array or an instance of \Traversable, '.
  611.                             '"%s" given.',
  612.                             $property,
  613.                             $reflClass->name,
  614.                             implode('()", "', [$methods[self::ACCESS_ADDER], $methods[self::ACCESS_REMOVER]]),
  615.                             \is_object($value) ? \get_class($value) : \gettype($value)
  616.                         );
  617.                     }
  618.                 }
  619.                 if (!isset($access[self::ACCESS_NAME])) {
  620.                     $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  621.                     $triedMethods = [
  622.                         $setter => 1,
  623.                         $getsetter => 1,
  624.                         '__set' => 2,
  625.                         '__call' => 2,
  626.                     ];
  627.                     foreach ($singulars as $singular) {
  628.                         $triedMethods['add'.$singular] = 1;
  629.                         $triedMethods['remove'.$singular] = 1;
  630.                     }
  631.                     foreach ($triedMethods as $methodName => $parameters) {
  632.                         if (!$reflClass->hasMethod($methodName)) {
  633.                             continue;
  634.                         }
  635.                         $method $reflClass->getMethod($methodName);
  636.                         if (!$method->isPublic()) {
  637.                             $errors[] = sprintf('The method "%s" in class "%s" was found but does not have public access'$methodName$reflClass->name);
  638.                             continue;
  639.                         }
  640.                         if ($method->getNumberOfRequiredParameters() > $parameters || $method->getNumberOfParameters() < $parameters) {
  641.                             $errors[] = sprintf('The method "%s" in class "%s" requires %d arguments, but should accept only %d'$methodName$reflClass->name$method->getNumberOfRequiredParameters(), $parameters);
  642.                         }
  643.                     }
  644.                     if (\count($errors)) {
  645.                         $access[self::ACCESS_NAME] = implode('. '$errors).'.';
  646.                     } else {
  647.                         $access[self::ACCESS_NAME] = sprintf(
  648.                             'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
  649.                             '"__set()" or "__call()" exist and have public access in class "%s".',
  650.                             $property,
  651.                             implode(''array_map(function ($singular) {
  652.                                 return '"add'.$singular.'()"/"remove'.$singular.'()", ';
  653.                             }, $singulars)),
  654.                             $setter,
  655.                             $getsetter,
  656.                             $reflClass->name
  657.                         );
  658.                     }
  659.                 }
  660.             }
  661.         }
  662.         if (isset($item)) {
  663.             $this->cacheItemPool->save($item->set($access));
  664.         }
  665.         return $this->writePropertyCache[$key] = $access;
  666.     }
  667.     /**
  668.      * Returns whether a property is writable in the given object.
  669.      *
  670.      * @param object $object The object to write to
  671.      */
  672.     private function isPropertyWritable($objectstring $property): bool
  673.     {
  674.         if (!\is_object($object)) {
  675.             return false;
  676.         }
  677.         $access $this->getWriteAccessInfo(\get_class($object), $property, []);
  678.         $isWritable self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
  679.             || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
  680.             || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
  681.             || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property))
  682.             || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
  683.         if ($isWritable) {
  684.             return true;
  685.         }
  686.         $access $this->getWriteAccessInfo(\get_class($object), $property'');
  687.         return self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
  688.             || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
  689.             || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
  690.             || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property))
  691.             || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
  692.     }
  693.     /**
  694.      * Camelizes a given string.
  695.      */
  696.     private function camelize(string $string): string
  697.     {
  698.         return str_replace(' '''ucwords(str_replace('_'' '$string)));
  699.     }
  700.     /**
  701.      * Searches for add and remove methods.
  702.      */
  703.     private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars): iterable
  704.     {
  705.         foreach ($singulars as $singular) {
  706.             $addMethod 'add'.$singular;
  707.             $removeMethod 'remove'.$singular;
  708.             $result = ['methods' => [self::ACCESS_ADDER => $addMethodself::ACCESS_REMOVER => $removeMethod]];
  709.             $addMethodFound $this->isMethodAccessible($reflClass$addMethod1);
  710.             if ($addMethodFound) {
  711.                 $result[self::ACCESS_ADDER] = $addMethod;
  712.             }
  713.             $removeMethodFound $this->isMethodAccessible($reflClass$removeMethod1);
  714.             if ($removeMethodFound) {
  715.                 $result[self::ACCESS_REMOVER] = $removeMethod;
  716.             }
  717.             yield $result;
  718.         }
  719.         return null;
  720.     }
  721.     /**
  722.      * Returns whether a method is public and has the number of required parameters.
  723.      */
  724.     private function isMethodAccessible(\ReflectionClass $classstring $methodNameint $parameters): bool
  725.     {
  726.         if ($class->hasMethod($methodName)) {
  727.             $method $class->getMethod($methodName);
  728.             if ($method->isPublic()
  729.                 && $method->getNumberOfRequiredParameters() <= $parameters
  730.                 && $method->getNumberOfParameters() >= $parameters) {
  731.                 return true;
  732.             }
  733.         }
  734.         return false;
  735.     }
  736.     /**
  737.      * Gets a PropertyPath instance and caches it.
  738.      *
  739.      * @param string|PropertyPath $propertyPath
  740.      */
  741.     private function getPropertyPath($propertyPath): PropertyPath
  742.     {
  743.         if ($propertyPath instanceof PropertyPathInterface) {
  744.             // Don't call the copy constructor has it is not needed here
  745.             return $propertyPath;
  746.         }
  747.         if (isset($this->propertyPathCache[$propertyPath])) {
  748.             return $this->propertyPathCache[$propertyPath];
  749.         }
  750.         if ($this->cacheItemPool) {
  751.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.rawurlencode($propertyPath));
  752.             if ($item->isHit()) {
  753.                 return $this->propertyPathCache[$propertyPath] = $item->get();
  754.             }
  755.         }
  756.         $propertyPathInstance = new PropertyPath($propertyPath);
  757.         if (isset($item)) {
  758.             $item->set($propertyPathInstance);
  759.             $this->cacheItemPool->save($item);
  760.         }
  761.         return $this->propertyPathCache[$propertyPath] = $propertyPathInstance;
  762.     }
  763.     /**
  764.      * Creates the APCu adapter if applicable.
  765.      *
  766.      * @param string $namespace
  767.      * @param int    $defaultLifetime
  768.      * @param string $version
  769.      *
  770.      * @return AdapterInterface
  771.      *
  772.      * @throws \LogicException When the Cache Component isn't available
  773.      */
  774.     public static function createCache($namespace$defaultLifetime$versionLoggerInterface $logger null)
  775.     {
  776.         if (null === $defaultLifetime) {
  777.             @trigger_error(sprintf('Passing null as "$defaultLifetime" 2nd argument of the "%s()" method is deprecated since Symfony 4.4, pass 0 instead.'__METHOD__), \E_USER_DEPRECATED);
  778.         }
  779.         if (!class_exists(ApcuAdapter::class)) {
  780.             throw new \LogicException(sprintf('The Symfony Cache component must be installed to use "%s()".'__METHOD__));
  781.         }
  782.         if (!ApcuAdapter::isSupported()) {
  783.             return new NullAdapter();
  784.         }
  785.         $apcu = new ApcuAdapter($namespace$defaultLifetime 5$version);
  786.         if ('cli' === \PHP_SAPI && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) {
  787.             $apcu->setLogger(new NullLogger());
  788.         } elseif (null !== $logger) {
  789.             $apcu->setLogger($logger);
  790.         }
  791.         return $apcu;
  792.     }
  793. }