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

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