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

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 (!str_starts_with($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.             [, $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::RESULT_PROTO;
  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::RESULT_PROTO;
  329.         $object $zval[self::VALUE];
  330.         $class = \get_class($object);
  331.         $access $this->getReadAccessInfo(\get_class($object), $property);
  332.         try {
  333.             if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
  334.                 try {
  335.                     $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
  336.                 } catch (\TypeError $e) {
  337.                     [$trace] = $e->getTrace();
  338.                     // handle uninitialized properties in PHP >= 7
  339.                     if (__FILE__ === $trace['file']
  340.                         && $access[self::ACCESS_NAME] === $trace['function']
  341.                         && $object instanceof $trace['class']
  342.                         && preg_match('/Return value (?:of .*::\w+\(\) )?must be of (?:the )?type (\w+), null returned$/'$e->getMessage(), $matches)
  343.                     ) {
  344.                         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"?', !str_contains(\get_class($object), "@anonymous\0") ? \get_class($object) : (get_parent_class($object) ?: 'class').'@anonymous'$access[self::ACCESS_NAME], $matches[1]), 0$e);
  345.                     }
  346.                     throw $e;
  347.                 }
  348.             } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
  349.                 $name $access[self::ACCESS_NAME];
  350.                 if ($access[self::ACCESS_REF] && !isset($object->$name) && !\array_key_exists($name, (array) $object) && (\PHP_VERSION_ID 70400 || !(new \ReflectionProperty($class$name))->hasType())) {
  351.                     throw new AccessException(sprintf('The property "%s::$%s" is not initialized.'$class$name));
  352.                 }
  353.                 $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
  354.                 if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
  355.                     $result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
  356.                 }
  357.             } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property)) {
  358.                 // Needed to support \stdClass instances. We need to explicitly
  359.                 // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
  360.                 // a *protected* property was found on the class, property_exists()
  361.                 // returns true, consequently the following line will result in a
  362.                 // fatal error.
  363.                 $result[self::VALUE] = $object->$property;
  364.                 if (isset($zval[self::REF])) {
  365.                     $result[self::REF] = &$object->$property;
  366.                 }
  367.             } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
  368.                 // we call the getter and hope the __call do the job
  369.                 $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
  370.             } elseif (!$ignoreInvalidProperty) {
  371.                 throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
  372.             }
  373.         } catch (\Error $e) {
  374.             // handle uninitialized properties in PHP >= 7.4
  375.             if (\PHP_VERSION_ID >= 70400 && preg_match('/^Typed property ('.preg_quote(get_debug_type($object), '/').')::\$(\w+) must not be accessed before initialization$/'$e->getMessage(), $matches)) {
  376.                 $r = new \ReflectionProperty($class$matches[2]);
  377.                 $type = ($type $r->getType()) instanceof \ReflectionNamedType $type->getName() : (string) $type;
  378.                 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);
  379.             }
  380.             throw $e;
  381.         }
  382.         // Objects are always passed around by reference
  383.         if (isset($zval[self::REF]) && \is_object($result[self::VALUE])) {
  384.             $result[self::REF] = $result[self::VALUE];
  385.         }
  386.         return $result;
  387.     }
  388.     /**
  389.      * Guesses how to read the property value.
  390.      */
  391.     private function getReadAccessInfo(string $classstring $property): array
  392.     {
  393.         $key str_replace('\\''.'$class).'..'.$property;
  394.         if (isset($this->readPropertyCache[$key])) {
  395.             return $this->readPropertyCache[$key];
  396.         }
  397.         if ($this->cacheItemPool) {
  398.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.rawurlencode($key));
  399.             if ($item->isHit()) {
  400.                 return $this->readPropertyCache[$key] = $item->get();
  401.             }
  402.         }
  403.         $access = [];
  404.         $reflClass = new \ReflectionClass($class);
  405.         $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
  406.         $camelProp $this->camelize($property);
  407.         $getter 'get'.$camelProp;
  408.         $getsetter lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
  409.         $isser 'is'.$camelProp;
  410.         $hasser 'has'.$camelProp;
  411.         $canAccessor 'can'.$camelProp;
  412.         if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
  413.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  414.             $access[self::ACCESS_NAME] = $getter;
  415.         } elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
  416.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  417.             $access[self::ACCESS_NAME] = $getsetter;
  418.         } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
  419.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  420.             $access[self::ACCESS_NAME] = $isser;
  421.         } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
  422.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  423.             $access[self::ACCESS_NAME] = $hasser;
  424.         } elseif ($reflClass->hasMethod($canAccessor) && $reflClass->getMethod($canAccessor)->isPublic()) {
  425.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  426.             $access[self::ACCESS_NAME] = $canAccessor;
  427.         } elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
  428.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  429.             $access[self::ACCESS_NAME] = $property;
  430.             $access[self::ACCESS_REF] = false;
  431.         } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
  432.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  433.             $access[self::ACCESS_NAME] = $property;
  434.             $access[self::ACCESS_REF] = true;
  435.         } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
  436.             // we call the getter and hope the __call do the job
  437.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
  438.             $access[self::ACCESS_NAME] = $getter;
  439.         } else {
  440.             $methods = [$getter$getsetter$isser$hasser'__get'];
  441.             if ($this->magicCall) {
  442.                 $methods[] = '__call';
  443.             }
  444.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  445.             $access[self::ACCESS_NAME] = sprintf(
  446.                 'Neither the property "%s" nor one of the methods "%s()" '.
  447.                 'exist and have public access in class "%s".',
  448.                 $property,
  449.                 implode('()", "'$methods),
  450.                 $reflClass->name
  451.             );
  452.         }
  453.         if (isset($item)) {
  454.             $this->cacheItemPool->save($item->set($access));
  455.         }
  456.         return $this->readPropertyCache[$key] = $access;
  457.     }
  458.     /**
  459.      * Sets the value of an index in a given array-accessible value.
  460.      *
  461.      * @param string|int $index The index to write at
  462.      * @param mixed      $value The value to write
  463.      *
  464.      * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
  465.      */
  466.     private function writeIndex(array $zval$index$value)
  467.     {
  468.         if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
  469.             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])));
  470.         }
  471.         $zval[self::REF][$index] = $value;
  472.     }
  473.     /**
  474.      * Sets the value of a property in the given object.
  475.      *
  476.      * @param mixed $value The value to write
  477.      *
  478.      * @throws NoSuchPropertyException if the property does not exist or is not public
  479.      */
  480.     private function writeProperty(array $zvalstring $property$value)
  481.     {
  482.         if (!\is_object($zval[self::VALUE])) {
  483.             throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%1$s]" instead?'$property));
  484.         }
  485.         $object $zval[self::VALUE];
  486.         $access $this->getWriteAccessInfo(\get_class($object), $property$value);
  487.         if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
  488.             $object->{$access[self::ACCESS_NAME]}($value);
  489.         } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
  490.             $object->{$access[self::ACCESS_NAME]} = $value;
  491.         } elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) {
  492.             $this->writeCollection($zval$property$value$access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]);
  493.         } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property)) {
  494.             // Needed to support \stdClass instances. We need to explicitly
  495.             // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
  496.             // a *protected* property was found on the class, property_exists()
  497.             // returns true, consequently the following line will result in a
  498.             // fatal error.
  499.             $object->$property $value;
  500.         } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
  501.             $object->{$access[self::ACCESS_NAME]}($value);
  502.         } elseif (self::ACCESS_TYPE_NOT_FOUND === $access[self::ACCESS_TYPE]) {
  503.             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] : '.'));
  504.         } else {
  505.             throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
  506.         }
  507.     }
  508.     /**
  509.      * Adjusts a collection-valued property by calling add*() and remove*() methods.
  510.      */
  511.     private function writeCollection(array $zvalstring $propertyiterable $collectionstring $addMethodstring $removeMethod)
  512.     {
  513.         // At this point the add and remove methods have been found
  514.         $previousValue $this->readProperty($zval$property);
  515.         $previousValue $previousValue[self::VALUE];
  516.         if ($previousValue instanceof \Traversable) {
  517.             $previousValue iterator_to_array($previousValue);
  518.         }
  519.         if ($previousValue && \is_array($previousValue)) {
  520.             if (\is_object($collection)) {
  521.                 $collection iterator_to_array($collection);
  522.             }
  523.             foreach ($previousValue as $key => $item) {
  524.                 if (!\in_array($item$collectiontrue)) {
  525.                     unset($previousValue[$key]);
  526.                     $zval[self::VALUE]->{$removeMethod}($item);
  527.                 }
  528.             }
  529.         } else {
  530.             $previousValue false;
  531.         }
  532.         foreach ($collection as $item) {
  533.             if (!$previousValue || !\in_array($item$previousValuetrue)) {
  534.                 $zval[self::VALUE]->{$addMethod}($item);
  535.             }
  536.         }
  537.     }
  538.     /**
  539.      * Guesses how to write the property value.
  540.      *
  541.      * @param mixed $value
  542.      */
  543.     private function getWriteAccessInfo(string $classstring $property$value): array
  544.     {
  545.         $useAdderAndRemover is_iterable($value);
  546.         $key str_replace('\\''.'$class).'..'.$property.'..'.(int) $useAdderAndRemover;
  547.         if (isset($this->writePropertyCache[$key])) {
  548.             return $this->writePropertyCache[$key];
  549.         }
  550.         if ($this->cacheItemPool) {
  551.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.rawurlencode($key));
  552.             if ($item->isHit()) {
  553.                 return $this->writePropertyCache[$key] = $item->get();
  554.             }
  555.         }
  556.         $access = [];
  557.         $reflClass = new \ReflectionClass($class);
  558.         $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
  559.         $camelized $this->camelize($property);
  560.         $singulars = (array) Inflector::singularize($camelized);
  561.         $errors = [];
  562.         if ($useAdderAndRemover) {
  563.             foreach ($this->findAdderAndRemover($reflClass$singulars) as $methods) {
  564.                 if (=== \count($methods)) {
  565.                     $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
  566.                     $access[self::ACCESS_ADDER] = $methods[self::ACCESS_ADDER];
  567.                     $access[self::ACCESS_REMOVER] = $methods[self::ACCESS_REMOVER];
  568.                     break;
  569.                 }
  570.                 if (isset($methods[self::ACCESS_ADDER])) {
  571.                     $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]);
  572.                 }
  573.                 if (isset($methods[self::ACCESS_REMOVER])) {
  574.                     $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]);
  575.                 }
  576.             }
  577.         }
  578.         if (!isset($access[self::ACCESS_TYPE])) {
  579.             $setter 'set'.$camelized;
  580.             $getsetter lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
  581.             if ($this->isMethodAccessible($reflClass$setter1)) {
  582.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  583.                 $access[self::ACCESS_NAME] = $setter;
  584.             } elseif ($this->isMethodAccessible($reflClass$getsetter1)) {
  585.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  586.                 $access[self::ACCESS_NAME] = $getsetter;
  587.             } elseif ($this->isMethodAccessible($reflClass'__set'2)) {
  588.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  589.                 $access[self::ACCESS_NAME] = $property;
  590.             } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
  591.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  592.                 $access[self::ACCESS_NAME] = $property;
  593.             } elseif ($this->magicCall && $this->isMethodAccessible($reflClass'__call'2)) {
  594.                 // we call the getter and hope the __call do the job
  595.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
  596.                 $access[self::ACCESS_NAME] = $setter;
  597.             } else {
  598.                 foreach ($this->findAdderAndRemover($reflClass$singulars) as $methods) {
  599.                     if (=== \count($methods)) {
  600.                         $errors[] = sprintf(
  601.                             'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
  602.                             'the new value must be an array or an instance of \Traversable, '.
  603.                             '"%s" given.',
  604.                             $property,
  605.                             $reflClass->name,
  606.                             implode('()", "', [$methods[self::ACCESS_ADDER], $methods[self::ACCESS_REMOVER]]),
  607.                             \is_object($value) ? \get_class($value) : \gettype($value)
  608.                         );
  609.                     }
  610.                 }
  611.                 if (!isset($access[self::ACCESS_NAME])) {
  612.                     $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  613.                     $triedMethods = [
  614.                         $setter => 1,
  615.                         $getsetter => 1,
  616.                         '__set' => 2,
  617.                         '__call' => 2,
  618.                     ];
  619.                     foreach ($singulars as $singular) {
  620.                         $triedMethods['add'.$singular] = 1;
  621.                         $triedMethods['remove'.$singular] = 1;
  622.                     }
  623.                     foreach ($triedMethods as $methodName => $parameters) {
  624.                         if (!$reflClass->hasMethod($methodName)) {
  625.                             continue;
  626.                         }
  627.                         $method $reflClass->getMethod($methodName);
  628.                         if (!$method->isPublic()) {
  629.                             $errors[] = sprintf('The method "%s" in class "%s" was found but does not have public access'$methodName$reflClass->name);
  630.                             continue;
  631.                         }
  632.                         if ($method->getNumberOfRequiredParameters() > $parameters || $method->getNumberOfParameters() < $parameters) {
  633.                             $errors[] = sprintf('The method "%s" in class "%s" requires %d arguments, but should accept only %d'$methodName$reflClass->name$method->getNumberOfRequiredParameters(), $parameters);
  634.                         }
  635.                     }
  636.                     if (\count($errors)) {
  637.                         $access[self::ACCESS_NAME] = implode('. '$errors).'.';
  638.                     } else {
  639.                         $access[self::ACCESS_NAME] = sprintf(
  640.                             'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
  641.                             '"__set()" or "__call()" exist and have public access in class "%s".',
  642.                             $property,
  643.                             implode(''array_map(function ($singular) {
  644.                                 return '"add'.$singular.'()"/"remove'.$singular.'()", ';
  645.                             }, $singulars)),
  646.                             $setter,
  647.                             $getsetter,
  648.                             $reflClass->name
  649.                         );
  650.                     }
  651.                 }
  652.             }
  653.         }
  654.         if (isset($item)) {
  655.             $this->cacheItemPool->save($item->set($access));
  656.         }
  657.         return $this->writePropertyCache[$key] = $access;
  658.     }
  659.     /**
  660.      * Returns whether a property is writable in the given object.
  661.      *
  662.      * @param object $object The object to write to
  663.      */
  664.     private function isPropertyWritable($objectstring $property): bool
  665.     {
  666.         if (!\is_object($object)) {
  667.             return false;
  668.         }
  669.         $access $this->getWriteAccessInfo(\get_class($object), $property, []);
  670.         $isWritable self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
  671.             || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
  672.             || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
  673.             || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property))
  674.             || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
  675.         if ($isWritable) {
  676.             return true;
  677.         }
  678.         $access $this->getWriteAccessInfo(\get_class($object), $property'');
  679.         return self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
  680.             || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
  681.             || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
  682.             || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property))
  683.             || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
  684.     }
  685.     /**
  686.      * Camelizes a given string.
  687.      */
  688.     private function camelize(string $string): string
  689.     {
  690.         return str_replace(' '''ucwords(str_replace('_'' '$string)));
  691.     }
  692.     /**
  693.      * Searches for add and remove methods.
  694.      */
  695.     private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars): iterable
  696.     {
  697.         foreach ($singulars as $singular) {
  698.             $addMethod 'add'.$singular;
  699.             $removeMethod 'remove'.$singular;
  700.             $result = ['methods' => [self::ACCESS_ADDER => $addMethodself::ACCESS_REMOVER => $removeMethod]];
  701.             $addMethodFound $this->isMethodAccessible($reflClass$addMethod1);
  702.             if ($addMethodFound) {
  703.                 $result[self::ACCESS_ADDER] = $addMethod;
  704.             }
  705.             $removeMethodFound $this->isMethodAccessible($reflClass$removeMethod1);
  706.             if ($removeMethodFound) {
  707.                 $result[self::ACCESS_REMOVER] = $removeMethod;
  708.             }
  709.             yield $result;
  710.         }
  711.         return null;
  712.     }
  713.     /**
  714.      * Returns whether a method is public and has the number of required parameters.
  715.      */
  716.     private function isMethodAccessible(\ReflectionClass $classstring $methodNameint $parameters): bool
  717.     {
  718.         if ($class->hasMethod($methodName)) {
  719.             $method $class->getMethod($methodName);
  720.             if ($method->isPublic()
  721.                 && $method->getNumberOfRequiredParameters() <= $parameters
  722.                 && $method->getNumberOfParameters() >= $parameters) {
  723.                 return true;
  724.             }
  725.         }
  726.         return false;
  727.     }
  728.     /**
  729.      * Gets a PropertyPath instance and caches it.
  730.      *
  731.      * @param string|PropertyPath $propertyPath
  732.      */
  733.     private function getPropertyPath($propertyPath): PropertyPath
  734.     {
  735.         if ($propertyPath instanceof PropertyPathInterface) {
  736.             // Don't call the copy constructor has it is not needed here
  737.             return $propertyPath;
  738.         }
  739.         if (isset($this->propertyPathCache[$propertyPath])) {
  740.             return $this->propertyPathCache[$propertyPath];
  741.         }
  742.         if ($this->cacheItemPool) {
  743.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.rawurlencode($propertyPath));
  744.             if ($item->isHit()) {
  745.                 return $this->propertyPathCache[$propertyPath] = $item->get();
  746.             }
  747.         }
  748.         $propertyPathInstance = new PropertyPath($propertyPath);
  749.         if (isset($item)) {
  750.             $item->set($propertyPathInstance);
  751.             $this->cacheItemPool->save($item);
  752.         }
  753.         return $this->propertyPathCache[$propertyPath] = $propertyPathInstance;
  754.     }
  755.     /**
  756.      * Creates the APCu adapter if applicable.
  757.      *
  758.      * @param string $namespace
  759.      * @param int    $defaultLifetime
  760.      * @param string $version
  761.      *
  762.      * @return AdapterInterface
  763.      *
  764.      * @throws \LogicException When the Cache Component isn't available
  765.      */
  766.     public static function createCache($namespace$defaultLifetime$versionLoggerInterface $logger null)
  767.     {
  768.         if (null === $defaultLifetime) {
  769.             @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);
  770.         }
  771.         if (!class_exists(ApcuAdapter::class)) {
  772.             throw new \LogicException(sprintf('The Symfony Cache component must be installed to use "%s()".'__METHOD__));
  773.         }
  774.         if (!ApcuAdapter::isSupported()) {
  775.             return new NullAdapter();
  776.         }
  777.         $apcu = new ApcuAdapter($namespace$defaultLifetime 5$version);
  778.         if ('cli' === \PHP_SAPI && !filter_var(ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) {
  779.             $apcu->setLogger(new NullLogger());
  780.         } elseif (null !== $logger) {
  781.             $apcu->setLogger($logger);
  782.         }
  783.         return $apcu;
  784.     }
  785. }