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

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 static $resultProto = [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);
  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 (!== strpos($message'Argument ')) {
  166.                 return;
  167.             }
  168.             $pos strpos($message$delim 'must be of the type ') ?: (strpos($message$delim 'must be an instance of ') ?: strpos($message$delim 'must implement interface '));
  169.             $pos += \strlen($delim);
  170.             $j strpos($message','$pos);
  171.             $type substr($message$jstrpos($message' given'$j) - $j 2);
  172.             $message substr($message$pos$j $pos);
  173.             throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".'$message'NULL' === $type 'null' $type$propertyPath), 0$previous);
  174.         }
  175.         if (preg_match('/^\S+::\S+\(\): Argument #\d+ \(\$\S+\) must be of type (\S+), (\S+) given/'$message$matches)) {
  176.             list(, $expectedType$actualType) = $matches;
  177.             throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".'$expectedType'NULL' === $actualType 'null' $actualType$propertyPath), 0$previous);
  178.         }
  179.     }
  180.     /**
  181.      * {@inheritdoc}
  182.      */
  183.     public function isReadable($objectOrArray$propertyPath)
  184.     {
  185.         if (!$propertyPath instanceof PropertyPathInterface) {
  186.             $propertyPath = new PropertyPath($propertyPath);
  187.         }
  188.         try {
  189.             $zval = [
  190.                 self::VALUE => $objectOrArray,
  191.             ];
  192.             $this->readPropertiesUntil($zval$propertyPath$propertyPath->getLength(), $this->ignoreInvalidIndices);
  193.             return true;
  194.         } catch (AccessException $e) {
  195.             return false;
  196.         } catch (UnexpectedTypeException $e) {
  197.             return false;
  198.         }
  199.     }
  200.     /**
  201.      * {@inheritdoc}
  202.      */
  203.     public function isWritable($objectOrArray$propertyPath)
  204.     {
  205.         $propertyPath $this->getPropertyPath($propertyPath);
  206.         try {
  207.             $zval = [
  208.                 self::VALUE => $objectOrArray,
  209.             ];
  210.             $propertyValues $this->readPropertiesUntil($zval$propertyPath$propertyPath->getLength() - 1);
  211.             for ($i = \count($propertyValues) - 1<= $i; --$i) {
  212.                 $zval $propertyValues[$i];
  213.                 unset($propertyValues[$i]);
  214.                 if ($propertyPath->isIndex($i)) {
  215.                     if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
  216.                         return false;
  217.                     }
  218.                 } else {
  219.                     if (!$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) {
  220.                         return false;
  221.                     }
  222.                 }
  223.                 if (\is_object($zval[self::VALUE])) {
  224.                     return true;
  225.                 }
  226.             }
  227.             return true;
  228.         } catch (AccessException $e) {
  229.             return false;
  230.         } catch (UnexpectedTypeException $e) {
  231.             return false;
  232.         }
  233.     }
  234.     /**
  235.      * Reads the path from an object up to a given path index.
  236.      *
  237.      * @throws UnexpectedTypeException if a value within the path is neither object nor array
  238.      * @throws NoSuchIndexException    If a non-existing index is accessed
  239.      */
  240.     private function readPropertiesUntil(array $zvalPropertyPathInterface $propertyPathint $lastIndexbool $ignoreInvalidIndices true): array
  241.     {
  242.         if (!\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) {
  243.             throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath0);
  244.         }
  245.         // Add the root object to the list
  246.         $propertyValues = [$zval];
  247.         for ($i 0$i $lastIndex; ++$i) {
  248.             $property $propertyPath->getElement($i);
  249.             $isIndex $propertyPath->isIndex($i);
  250.             if ($isIndex) {
  251.                 // Create missing nested arrays on demand
  252.                 if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
  253.                     (\is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !\array_key_exists($property$zval[self::VALUE]))
  254.                 ) {
  255.                     if (!$ignoreInvalidIndices) {
  256.                         if (!\is_array($zval[self::VALUE])) {
  257.                             if (!$zval[self::VALUE] instanceof \Traversable) {
  258.                                 throw new NoSuchIndexException(sprintf('Cannot read index "%s" while trying to traverse path "%s".'$property, (string) $propertyPath));
  259.                             }
  260.                             $zval[self::VALUE] = iterator_to_array($zval[self::VALUE]);
  261.                         }
  262.                         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)));
  263.                     }
  264.                     if ($i $propertyPath->getLength()) {
  265.                         if (isset($zval[self::REF])) {
  266.                             $zval[self::VALUE][$property] = [];
  267.                             $zval[self::REF] = $zval[self::VALUE];
  268.                         } else {
  269.                             $zval[self::VALUE] = [$property => []];
  270.                         }
  271.                     }
  272.                 }
  273.                 $zval $this->readIndex($zval$property);
  274.             } else {
  275.                 $zval $this->readProperty($zval$property$this->ignoreInvalidProperty);
  276.             }
  277.             // the final value of the path must not be validated
  278.             if ($i $propertyPath->getLength() && !\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) {
  279.                 throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath$i 1);
  280.             }
  281.             if (isset($zval[self::REF]) && (=== $i || isset($propertyValues[$i 1][self::IS_REF_CHAINED]))) {
  282.                 // Set the IS_REF_CHAINED flag to true if:
  283.                 // current property is passed by reference and
  284.                 // it is the first element in the property path or
  285.                 // the IS_REF_CHAINED flag of its parent element is true
  286.                 // Basically, this flag is true only when the reference chain from the top element to current element is not broken
  287.                 $zval[self::IS_REF_CHAINED] = true;
  288.             }
  289.             $propertyValues[] = $zval;
  290.         }
  291.         return $propertyValues;
  292.     }
  293.     /**
  294.      * Reads a key from an array-like structure.
  295.      *
  296.      * @param string|int $index The key to read
  297.      *
  298.      * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
  299.      */
  300.     private function readIndex(array $zval$index): array
  301.     {
  302.         if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
  303.             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])));
  304.         }
  305.         $result self::$resultProto;
  306.         if (isset($zval[self::VALUE][$index])) {
  307.             $result[self::VALUE] = $zval[self::VALUE][$index];
  308.             if (!isset($zval[self::REF])) {
  309.                 // Save creating references when doing read-only lookups
  310.             } elseif (\is_array($zval[self::VALUE])) {
  311.                 $result[self::REF] = &$zval[self::REF][$index];
  312.             } elseif (\is_object($result[self::VALUE])) {
  313.                 $result[self::REF] = $result[self::VALUE];
  314.             }
  315.         }
  316.         return $result;
  317.     }
  318.     /**
  319.      * Reads the a property from an object.
  320.      *
  321.      * @throws NoSuchPropertyException If $ignoreInvalidProperty is false and the property does not exist or is not public
  322.      */
  323.     private function readProperty(array $zvalstring $propertybool $ignoreInvalidProperty false): array
  324.     {
  325.         if (!\is_object($zval[self::VALUE])) {
  326.             throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.'$property));
  327.         }
  328.         $result self::$resultProto;
  329.         $object $zval[self::VALUE];
  330.         $access $this->getReadAccessInfo(\get_class($object), $property);
  331.         try {
  332.             if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
  333.                 try {
  334.                     $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
  335.                 } catch (\TypeError $e) {
  336.                     list($trace) = $e->getTrace();
  337.                     // handle uninitialized properties in PHP >= 7
  338.                     if (__FILE__ === $trace['file']
  339.                         && $access[self::ACCESS_NAME] === $trace['function']
  340.                         && $object instanceof $trace['class']
  341.                         && preg_match((sprintf('/Return value (?:of .*::\w+\(\) )?must be of (?:the )?type (\w+), null returned$/')), $e->getMessage(), $matches)
  342.                     ) {
  343.                         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"?'false === strpos(\get_class($object), "@anonymous\0") ? \get_class($object) : (get_parent_class($object) ?: 'class').'@anonymous'$access[self::ACCESS_NAME], $matches[1]), 0$e);
  344.                     }
  345.                     throw $e;
  346.                 }
  347.             } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
  348.                 $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
  349.                 if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
  350.                     $result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
  351.                 }
  352.             } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property)) {
  353.                 // Needed to support \stdClass instances. We need to explicitly
  354.                 // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
  355.                 // a *protected* property was found on the class, property_exists()
  356.                 // returns true, consequently the following line will result in a
  357.                 // fatal error.
  358.                 $result[self::VALUE] = $object->$property;
  359.                 if (isset($zval[self::REF])) {
  360.                     $result[self::REF] = &$object->$property;
  361.                 }
  362.             } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
  363.                 // we call the getter and hope the __call do the job
  364.                 $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
  365.             } elseif (!$ignoreInvalidProperty) {
  366.                 throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
  367.             }
  368.         } catch (\Error $e) {
  369.             // handle uninitialized properties in PHP >= 7.4
  370.             if (\PHP_VERSION_ID >= 70400 && preg_match('/^Typed property ([\w\\\]+)::\$(\w+) must not be accessed before initialization$/'$e->getMessage(), $matches)) {
  371.                 $r = new \ReflectionProperty($matches[1], $matches[2]);
  372.                 $type = ($type $r->getType()) instanceof \ReflectionNamedType $type->getName() : (string) $type;
  373.                 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.'$r->getDeclaringClass()->getName(), $r->getName(), $type), 0$e);
  374.             }
  375.             throw $e;
  376.         }
  377.         // Objects are always passed around by reference
  378.         if (isset($zval[self::REF]) && \is_object($result[self::VALUE])) {
  379.             $result[self::REF] = $result[self::VALUE];
  380.         }
  381.         return $result;
  382.     }
  383.     /**
  384.      * Guesses how to read the property value.
  385.      */
  386.     private function getReadAccessInfo(string $classstring $property): array
  387.     {
  388.         $key str_replace('\\''.'$class).'..'.$property;
  389.         if (isset($this->readPropertyCache[$key])) {
  390.             return $this->readPropertyCache[$key];
  391.         }
  392.         if ($this->cacheItemPool) {
  393.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.rawurlencode($key));
  394.             if ($item->isHit()) {
  395.                 return $this->readPropertyCache[$key] = $item->get();
  396.             }
  397.         }
  398.         $access = [];
  399.         $reflClass = new \ReflectionClass($class);
  400.         $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
  401.         $camelProp $this->camelize($property);
  402.         $getter 'get'.$camelProp;
  403.         $getsetter lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
  404.         $isser 'is'.$camelProp;
  405.         $hasser 'has'.$camelProp;
  406.         $canAccessor 'can'.$camelProp;
  407.         if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
  408.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  409.             $access[self::ACCESS_NAME] = $getter;
  410.         } elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
  411.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  412.             $access[self::ACCESS_NAME] = $getsetter;
  413.         } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
  414.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  415.             $access[self::ACCESS_NAME] = $isser;
  416.         } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
  417.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  418.             $access[self::ACCESS_NAME] = $hasser;
  419.         } elseif ($reflClass->hasMethod($canAccessor) && $reflClass->getMethod($canAccessor)->isPublic()) {
  420.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  421.             $access[self::ACCESS_NAME] = $canAccessor;
  422.         } elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
  423.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  424.             $access[self::ACCESS_NAME] = $property;
  425.             $access[self::ACCESS_REF] = false;
  426.         } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
  427.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  428.             $access[self::ACCESS_NAME] = $property;
  429.             $access[self::ACCESS_REF] = true;
  430.         } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
  431.             // we call the getter and hope the __call do the job
  432.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
  433.             $access[self::ACCESS_NAME] = $getter;
  434.         } else {
  435.             $methods = [$getter$getsetter$isser$hasser'__get'];
  436.             if ($this->magicCall) {
  437.                 $methods[] = '__call';
  438.             }
  439.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  440.             $access[self::ACCESS_NAME] = sprintf(
  441.                 'Neither the property "%s" nor one of the methods "%s()" '.
  442.                 'exist and have public access in class "%s".',
  443.                 $property,
  444.                 implode('()", "'$methods),
  445.                 $reflClass->name
  446.             );
  447.         }
  448.         if (isset($item)) {
  449.             $this->cacheItemPool->save($item->set($access));
  450.         }
  451.         return $this->readPropertyCache[$key] = $access;
  452.     }
  453.     /**
  454.      * Sets the value of an index in a given array-accessible value.
  455.      *
  456.      * @param string|int $index The index to write at
  457.      * @param mixed      $value The value to write
  458.      *
  459.      * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
  460.      */
  461.     private function writeIndex(array $zval$index$value)
  462.     {
  463.         if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
  464.             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])));
  465.         }
  466.         $zval[self::REF][$index] = $value;
  467.     }
  468.     /**
  469.      * Sets the value of a property in the given object.
  470.      *
  471.      * @param mixed $value The value to write
  472.      *
  473.      * @throws NoSuchPropertyException if the property does not exist or is not public
  474.      */
  475.     private function writeProperty(array $zvalstring $property$value)
  476.     {
  477.         if (!\is_object($zval[self::VALUE])) {
  478.             throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%1$s]" instead?'$property));
  479.         }
  480.         $object $zval[self::VALUE];
  481.         $access $this->getWriteAccessInfo(\get_class($object), $property$value);
  482.         if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
  483.             $object->{$access[self::ACCESS_NAME]}($value);
  484.         } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
  485.             $object->{$access[self::ACCESS_NAME]} = $value;
  486.         } elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) {
  487.             $this->writeCollection($zval$property$value$access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]);
  488.         } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property)) {
  489.             // Needed to support \stdClass instances. We need to explicitly
  490.             // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
  491.             // a *protected* property was found on the class, property_exists()
  492.             // returns true, consequently the following line will result in a
  493.             // fatal error.
  494.             $object->$property $value;
  495.         } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
  496.             $object->{$access[self::ACCESS_NAME]}($value);
  497.         } elseif (self::ACCESS_TYPE_NOT_FOUND === $access[self::ACCESS_TYPE]) {
  498.             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] : '.'));
  499.         } else {
  500.             throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
  501.         }
  502.     }
  503.     /**
  504.      * Adjusts a collection-valued property by calling add*() and remove*() methods.
  505.      */
  506.     private function writeCollection(array $zvalstring $propertyiterable $collectionstring $addMethodstring $removeMethod)
  507.     {
  508.         // At this point the add and remove methods have been found
  509.         $previousValue $this->readProperty($zval$property);
  510.         $previousValue $previousValue[self::VALUE];
  511.         if ($previousValue instanceof \Traversable) {
  512.             $previousValue iterator_to_array($previousValue);
  513.         }
  514.         if ($previousValue && \is_array($previousValue)) {
  515.             if (\is_object($collection)) {
  516.                 $collection iterator_to_array($collection);
  517.             }
  518.             foreach ($previousValue as $key => $item) {
  519.                 if (!\in_array($item$collectiontrue)) {
  520.                     unset($previousValue[$key]);
  521.                     $zval[self::VALUE]->{$removeMethod}($item);
  522.                 }
  523.             }
  524.         } else {
  525.             $previousValue false;
  526.         }
  527.         foreach ($collection as $item) {
  528.             if (!$previousValue || !\in_array($item$previousValuetrue)) {
  529.                 $zval[self::VALUE]->{$addMethod}($item);
  530.             }
  531.         }
  532.     }
  533.     /**
  534.      * Guesses how to write the property value.
  535.      *
  536.      * @param mixed $value
  537.      */
  538.     private function getWriteAccessInfo(string $classstring $property$value): array
  539.     {
  540.         $useAdderAndRemover = \is_array($value) || $value instanceof \Traversable;
  541.         $key str_replace('\\''.'$class).'..'.$property.'..'.(int) $useAdderAndRemover;
  542.         if (isset($this->writePropertyCache[$key])) {
  543.             return $this->writePropertyCache[$key];
  544.         }
  545.         if ($this->cacheItemPool) {
  546.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.rawurlencode($key));
  547.             if ($item->isHit()) {
  548.                 return $this->writePropertyCache[$key] = $item->get();
  549.             }
  550.         }
  551.         $access = [];
  552.         $reflClass = new \ReflectionClass($class);
  553.         $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
  554.         $camelized $this->camelize($property);
  555.         $singulars = (array) Inflector::singularize($camelized);
  556.         $errors = [];
  557.         if ($useAdderAndRemover) {
  558.             foreach ($this->findAdderAndRemover($reflClass$singulars) as $methods) {
  559.                 if (=== \count($methods)) {
  560.                     $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
  561.                     $access[self::ACCESS_ADDER] = $methods[self::ACCESS_ADDER];
  562.                     $access[self::ACCESS_REMOVER] = $methods[self::ACCESS_REMOVER];
  563.                     break;
  564.                 }
  565.                 if (isset($methods[self::ACCESS_ADDER])) {
  566.                     $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]);
  567.                 }
  568.                 if (isset($methods[self::ACCESS_REMOVER])) {
  569.                     $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]);
  570.                 }
  571.             }
  572.         }
  573.         if (!isset($access[self::ACCESS_TYPE])) {
  574.             $setter 'set'.$camelized;
  575.             $getsetter lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
  576.             if ($this->isMethodAccessible($reflClass$setter1)) {
  577.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  578.                 $access[self::ACCESS_NAME] = $setter;
  579.             } elseif ($this->isMethodAccessible($reflClass$getsetter1)) {
  580.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  581.                 $access[self::ACCESS_NAME] = $getsetter;
  582.             } elseif ($this->isMethodAccessible($reflClass'__set'2)) {
  583.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  584.                 $access[self::ACCESS_NAME] = $property;
  585.             } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
  586.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  587.                 $access[self::ACCESS_NAME] = $property;
  588.             } elseif ($this->magicCall && $this->isMethodAccessible($reflClass'__call'2)) {
  589.                 // we call the getter and hope the __call do the job
  590.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
  591.                 $access[self::ACCESS_NAME] = $setter;
  592.             } else {
  593.                 foreach ($this->findAdderAndRemover($reflClass$singulars) as $methods) {
  594.                     if (=== \count($methods)) {
  595.                         $errors[] = sprintf(
  596.                             'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
  597.                             'the new value must be an array or an instance of \Traversable, '.
  598.                             '"%s" given.',
  599.                             $property,
  600.                             $reflClass->name,
  601.                             implode('()", "', [$methods[self::ACCESS_ADDER], $methods[self::ACCESS_REMOVER]]),
  602.                             \is_object($value) ? \get_class($value) : \gettype($value)
  603.                         );
  604.                     }
  605.                 }
  606.                 if (!isset($access[self::ACCESS_NAME])) {
  607.                     $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  608.                     $triedMethods = [
  609.                         $setter => 1,
  610.                         $getsetter => 1,
  611.                         '__set' => 2,
  612.                         '__call' => 2,
  613.                     ];
  614.                     foreach ($singulars as $singular) {
  615.                         $triedMethods['add'.$singular] = 1;
  616.                         $triedMethods['remove'.$singular] = 1;
  617.                     }
  618.                     foreach ($triedMethods as $methodName => $parameters) {
  619.                         if (!$reflClass->hasMethod($methodName)) {
  620.                             continue;
  621.                         }
  622.                         $method $reflClass->getMethod($methodName);
  623.                         if (!$method->isPublic()) {
  624.                             $errors[] = sprintf('The method "%s" in class "%s" was found but does not have public access'$methodName$reflClass->name);
  625.                             continue;
  626.                         }
  627.                         if ($method->getNumberOfRequiredParameters() > $parameters || $method->getNumberOfParameters() < $parameters) {
  628.                             $errors[] = sprintf('The method "%s" in class "%s" requires %d arguments, but should accept only %d'$methodName$reflClass->name$method->getNumberOfRequiredParameters(), $parameters);
  629.                         }
  630.                     }
  631.                     if (\count($errors)) {
  632.                         $access[self::ACCESS_NAME] = implode('. '$errors).'.';
  633.                     } else {
  634.                         $access[self::ACCESS_NAME] = sprintf(
  635.                             'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
  636.                             '"__set()" or "__call()" exist and have public access in class "%s".',
  637.                             $property,
  638.                             implode(''array_map(function ($singular) {
  639.                                 return '"add'.$singular.'()"/"remove'.$singular.'()", ';
  640.                             }, $singulars)),
  641.                             $setter,
  642.                             $getsetter,
  643.                             $reflClass->name
  644.                         );
  645.                     }
  646.                 }
  647.             }
  648.         }
  649.         if (isset($item)) {
  650.             $this->cacheItemPool->save($item->set($access));
  651.         }
  652.         return $this->writePropertyCache[$key] = $access;
  653.     }
  654.     /**
  655.      * Returns whether a property is writable in the given object.
  656.      *
  657.      * @param object $object The object to write to
  658.      */
  659.     private function isPropertyWritable($objectstring $property): bool
  660.     {
  661.         if (!\is_object($object)) {
  662.             return false;
  663.         }
  664.         $access $this->getWriteAccessInfo(\get_class($object), $property, []);
  665.         $isWritable self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
  666.             || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
  667.             || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
  668.             || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property))
  669.             || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
  670.         if ($isWritable) {
  671.             return true;
  672.         }
  673.         $access $this->getWriteAccessInfo(\get_class($object), $property'');
  674.         return self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
  675.             || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
  676.             || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
  677.             || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property))
  678.             || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
  679.     }
  680.     /**
  681.      * Camelizes a given string.
  682.      */
  683.     private function camelize(string $string): string
  684.     {
  685.         return str_replace(' '''ucwords(str_replace('_'' '$string)));
  686.     }
  687.     /**
  688.      * Searches for add and remove methods.
  689.      */
  690.     private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars): iterable
  691.     {
  692.         foreach ($singulars as $singular) {
  693.             $addMethod 'add'.$singular;
  694.             $removeMethod 'remove'.$singular;
  695.             $result = ['methods' => [self::ACCESS_ADDER => $addMethodself::ACCESS_REMOVER => $removeMethod]];
  696.             $addMethodFound $this->isMethodAccessible($reflClass$addMethod1);
  697.             if ($addMethodFound) {
  698.                 $result[self::ACCESS_ADDER] = $addMethod;
  699.             }
  700.             $removeMethodFound $this->isMethodAccessible($reflClass$removeMethod1);
  701.             if ($removeMethodFound) {
  702.                 $result[self::ACCESS_REMOVER] = $removeMethod;
  703.             }
  704.             yield $result;
  705.         }
  706.         return null;
  707.     }
  708.     /**
  709.      * Returns whether a method is public and has the number of required parameters.
  710.      */
  711.     private function isMethodAccessible(\ReflectionClass $classstring $methodNameint $parameters): bool
  712.     {
  713.         if ($class->hasMethod($methodName)) {
  714.             $method $class->getMethod($methodName);
  715.             if ($method->isPublic()
  716.                 && $method->getNumberOfRequiredParameters() <= $parameters
  717.                 && $method->getNumberOfParameters() >= $parameters) {
  718.                 return true;
  719.             }
  720.         }
  721.         return false;
  722.     }
  723.     /**
  724.      * Gets a PropertyPath instance and caches it.
  725.      *
  726.      * @param string|PropertyPath $propertyPath
  727.      */
  728.     private function getPropertyPath($propertyPath): PropertyPath
  729.     {
  730.         if ($propertyPath instanceof PropertyPathInterface) {
  731.             // Don't call the copy constructor has it is not needed here
  732.             return $propertyPath;
  733.         }
  734.         if (isset($this->propertyPathCache[$propertyPath])) {
  735.             return $this->propertyPathCache[$propertyPath];
  736.         }
  737.         if ($this->cacheItemPool) {
  738.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.rawurlencode($propertyPath));
  739.             if ($item->isHit()) {
  740.                 return $this->propertyPathCache[$propertyPath] = $item->get();
  741.             }
  742.         }
  743.         $propertyPathInstance = new PropertyPath($propertyPath);
  744.         if (isset($item)) {
  745.             $item->set($propertyPathInstance);
  746.             $this->cacheItemPool->save($item);
  747.         }
  748.         return $this->propertyPathCache[$propertyPath] = $propertyPathInstance;
  749.     }
  750.     /**
  751.      * Creates the APCu adapter if applicable.
  752.      *
  753.      * @param string $namespace
  754.      * @param int    $defaultLifetime
  755.      * @param string $version
  756.      *
  757.      * @return AdapterInterface
  758.      *
  759.      * @throws \LogicException When the Cache Component isn't available
  760.      */
  761.     public static function createCache($namespace$defaultLifetime$versionLoggerInterface $logger null)
  762.     {
  763.         if (null === $defaultLifetime) {
  764.             @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);
  765.         }
  766.         if (!class_exists('Symfony\Component\Cache\Adapter\ApcuAdapter')) {
  767.             throw new \LogicException(sprintf('The Symfony Cache component must be installed to use "%s()".'__METHOD__));
  768.         }
  769.         if (!ApcuAdapter::isSupported()) {
  770.             return new NullAdapter();
  771.         }
  772.         $apcu = new ApcuAdapter($namespace$defaultLifetime 5$version);
  773.         if ('cli' === \PHP_SAPI && !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN)) {
  774.             $apcu->setLogger(new NullLogger());
  775.         } elseif (null !== $logger) {
  776.             $apcu->setLogger($logger);
  777.         }
  778.         return $apcu;
  779.     }
  780. }