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

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.     /**
  55.      * @var CacheItemPoolInterface
  56.      */
  57.     private $cacheItemPool;
  58.     private $propertyPathCache = [];
  59.     private $readPropertyCache = [];
  60.     private $writePropertyCache = [];
  61.     private static $resultProto = [self::VALUE => null];
  62.     /**
  63.      * Should not be used by application code. Use
  64.      * {@link PropertyAccess::createPropertyAccessor()} instead.
  65.      */
  66.     public function __construct(bool $magicCall falsebool $throwExceptionOnInvalidIndex falseCacheItemPoolInterface $cacheItemPool null)
  67.     {
  68.         $this->magicCall $magicCall;
  69.         $this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex;
  70.         $this->cacheItemPool $cacheItemPool instanceof NullAdapter null $cacheItemPool// Replace the NullAdapter by the null value
  71.     }
  72.     /**
  73.      * {@inheritdoc}
  74.      */
  75.     public function getValue($objectOrArray$propertyPath)
  76.     {
  77.         $propertyPath $this->getPropertyPath($propertyPath);
  78.         $zval = [
  79.             self::VALUE => $objectOrArray,
  80.         ];
  81.         $propertyValues $this->readPropertiesUntil($zval$propertyPath$propertyPath->getLength(), $this->ignoreInvalidIndices);
  82.         return $propertyValues[\count($propertyValues) - 1][self::VALUE];
  83.     }
  84.     /**
  85.      * {@inheritdoc}
  86.      */
  87.     public function setValue(&$objectOrArray$propertyPath$value)
  88.     {
  89.         $propertyPath $this->getPropertyPath($propertyPath);
  90.         $zval = [
  91.             self::VALUE => $objectOrArray,
  92.             self::REF => &$objectOrArray,
  93.         ];
  94.         $propertyValues $this->readPropertiesUntil($zval$propertyPath$propertyPath->getLength() - 1);
  95.         $overwrite true;
  96.         try {
  97.             for ($i = \count($propertyValues) - 1<= $i; --$i) {
  98.                 $zval $propertyValues[$i];
  99.                 unset($propertyValues[$i]);
  100.                 // You only need set value for current element if:
  101.                 // 1. it's the parent of the last index element
  102.                 // OR
  103.                 // 2. its child is not passed by reference
  104.                 //
  105.                 // This may avoid uncessary value setting process for array elements.
  106.                 // For example:
  107.                 // '[a][b][c]' => 'old-value'
  108.                 // If you want to change its value to 'new-value',
  109.                 // you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]'
  110.                 if ($overwrite) {
  111.                     $property $propertyPath->getElement($i);
  112.                     if ($propertyPath->isIndex($i)) {
  113.                         if ($overwrite = !isset($zval[self::REF])) {
  114.                             $ref = &$zval[self::REF];
  115.                             $ref $zval[self::VALUE];
  116.                         }
  117.                         $this->writeIndex($zval$property$value);
  118.                         if ($overwrite) {
  119.                             $zval[self::VALUE] = $zval[self::REF];
  120.                         }
  121.                     } else {
  122.                         $this->writeProperty($zval$property$value);
  123.                     }
  124.                     // if current element is an object
  125.                     // OR
  126.                     // if current element's reference chain is not broken - current element
  127.                     // as well as all its ancients in the property path are all passed by reference,
  128.                     // then there is no need to continue the value setting process
  129.                     if (\is_object($zval[self::VALUE]) || isset($zval[self::IS_REF_CHAINED])) {
  130.                         break;
  131.                     }
  132.                 }
  133.                 $value $zval[self::VALUE];
  134.             }
  135.         } catch (\TypeError $e) {
  136.             self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0$propertyPath);
  137.             // It wasn't thrown in this class so rethrow it
  138.             throw $e;
  139.         }
  140.     }
  141.     private static function throwInvalidArgumentException($message$trace$i$propertyPath)
  142.     {
  143.         // the type mismatch is not caused by invalid arguments (but e.g. by an incompatible return type hint of the writer method)
  144.         if (!== strpos($message'Argument ')) {
  145.             return;
  146.         }
  147.         if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file'] && \array_key_exists(0$trace[$i]['args'])) {
  148.             $pos strpos($message$delim 'must be of the type ') ?: (strpos($message$delim 'must be an instance of ') ?: strpos($message$delim 'must implement interface '));
  149.             $pos += \strlen($delim);
  150.             $type $trace[$i]['args'][0];
  151.             $type = \is_object($type) ? \get_class($type) : \gettype($type);
  152.             throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".'substr($message$posstrpos($message','$pos) - $pos), $type$propertyPath));
  153.         }
  154.     }
  155.     /**
  156.      * {@inheritdoc}
  157.      */
  158.     public function isReadable($objectOrArray$propertyPath)
  159.     {
  160.         if (!$propertyPath instanceof PropertyPathInterface) {
  161.             $propertyPath = new PropertyPath($propertyPath);
  162.         }
  163.         try {
  164.             $zval = [
  165.                 self::VALUE => $objectOrArray,
  166.             ];
  167.             $this->readPropertiesUntil($zval$propertyPath$propertyPath->getLength(), $this->ignoreInvalidIndices);
  168.             return true;
  169.         } catch (AccessException $e) {
  170.             return false;
  171.         } catch (UnexpectedTypeException $e) {
  172.             return false;
  173.         }
  174.     }
  175.     /**
  176.      * {@inheritdoc}
  177.      */
  178.     public function isWritable($objectOrArray$propertyPath)
  179.     {
  180.         $propertyPath $this->getPropertyPath($propertyPath);
  181.         try {
  182.             $zval = [
  183.                 self::VALUE => $objectOrArray,
  184.             ];
  185.             $propertyValues $this->readPropertiesUntil($zval$propertyPath$propertyPath->getLength() - 1);
  186.             for ($i = \count($propertyValues) - 1<= $i; --$i) {
  187.                 $zval $propertyValues[$i];
  188.                 unset($propertyValues[$i]);
  189.                 if ($propertyPath->isIndex($i)) {
  190.                     if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
  191.                         return false;
  192.                     }
  193.                 } else {
  194.                     if (!$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) {
  195.                         return false;
  196.                     }
  197.                 }
  198.                 if (\is_object($zval[self::VALUE])) {
  199.                     return true;
  200.                 }
  201.             }
  202.             return true;
  203.         } catch (AccessException $e) {
  204.             return false;
  205.         } catch (UnexpectedTypeException $e) {
  206.             return false;
  207.         }
  208.     }
  209.     /**
  210.      * Reads the path from an object up to a given path index.
  211.      *
  212.      * @param array                 $zval                 The array containing the object or array to read from
  213.      * @param PropertyPathInterface $propertyPath         The property path to read
  214.      * @param int                   $lastIndex            The index up to which should be read
  215.      * @param bool                  $ignoreInvalidIndices Whether to ignore invalid indices or throw an exception
  216.      *
  217.      * @return array The values read in the path
  218.      *
  219.      * @throws UnexpectedTypeException if a value within the path is neither object nor array
  220.      * @throws NoSuchIndexException    If a non-existing index is accessed
  221.      */
  222.     private function readPropertiesUntil($zvalPropertyPathInterface $propertyPath$lastIndex$ignoreInvalidIndices true)
  223.     {
  224.         if (!\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) {
  225.             throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath0);
  226.         }
  227.         // Add the root object to the list
  228.         $propertyValues = [$zval];
  229.         for ($i 0$i $lastIndex; ++$i) {
  230.             $property $propertyPath->getElement($i);
  231.             $isIndex $propertyPath->isIndex($i);
  232.             if ($isIndex) {
  233.                 // Create missing nested arrays on demand
  234.                 if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
  235.                     (\is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !\array_key_exists($property$zval[self::VALUE]))
  236.                 ) {
  237.                     if (!$ignoreInvalidIndices) {
  238.                         if (!\is_array($zval[self::VALUE])) {
  239.                             if (!$zval[self::VALUE] instanceof \Traversable) {
  240.                                 throw new NoSuchIndexException(sprintf('Cannot read index "%s" while trying to traverse path "%s".'$property, (string) $propertyPath));
  241.                             }
  242.                             $zval[self::VALUE] = iterator_to_array($zval[self::VALUE]);
  243.                         }
  244.                         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)));
  245.                     }
  246.                     if ($i $propertyPath->getLength()) {
  247.                         if (isset($zval[self::REF])) {
  248.                             $zval[self::VALUE][$property] = [];
  249.                             $zval[self::REF] = $zval[self::VALUE];
  250.                         } else {
  251.                             $zval[self::VALUE] = [$property => []];
  252.                         }
  253.                     }
  254.                 }
  255.                 $zval $this->readIndex($zval$property);
  256.             } else {
  257.                 $zval $this->readProperty($zval$property);
  258.             }
  259.             // the final value of the path must not be validated
  260.             if ($i $propertyPath->getLength() && !\is_object($zval[self::VALUE]) && !\is_array($zval[self::VALUE])) {
  261.                 throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath$i 1);
  262.             }
  263.             if (isset($zval[self::REF]) && (=== $i || isset($propertyValues[$i 1][self::IS_REF_CHAINED]))) {
  264.                 // Set the IS_REF_CHAINED flag to true if:
  265.                 // current property is passed by reference and
  266.                 // it is the first element in the property path or
  267.                 // the IS_REF_CHAINED flag of its parent element is true
  268.                 // Basically, this flag is true only when the reference chain from the top element to current element is not broken
  269.                 $zval[self::IS_REF_CHAINED] = true;
  270.             }
  271.             $propertyValues[] = $zval;
  272.         }
  273.         return $propertyValues;
  274.     }
  275.     /**
  276.      * Reads a key from an array-like structure.
  277.      *
  278.      * @param array      $zval  The array containing the array or \ArrayAccess object to read from
  279.      * @param string|int $index The key to read
  280.      *
  281.      * @return array The array containing the value of the key
  282.      *
  283.      * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
  284.      */
  285.     private function readIndex($zval$index)
  286.     {
  287.         if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
  288.             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])));
  289.         }
  290.         $result self::$resultProto;
  291.         if (isset($zval[self::VALUE][$index])) {
  292.             $result[self::VALUE] = $zval[self::VALUE][$index];
  293.             if (!isset($zval[self::REF])) {
  294.                 // Save creating references when doing read-only lookups
  295.             } elseif (\is_array($zval[self::VALUE])) {
  296.                 $result[self::REF] = &$zval[self::REF][$index];
  297.             } elseif (\is_object($result[self::VALUE])) {
  298.                 $result[self::REF] = $result[self::VALUE];
  299.             }
  300.         }
  301.         return $result;
  302.     }
  303.     /**
  304.      * Reads the a property from an object.
  305.      *
  306.      * @param array  $zval     The array containing the object to read from
  307.      * @param string $property The property to read
  308.      *
  309.      * @return array The array containing the value of the property
  310.      *
  311.      * @throws NoSuchPropertyException if the property does not exist or is not public
  312.      */
  313.     private function readProperty($zval$property)
  314.     {
  315.         if (!\is_object($zval[self::VALUE])) {
  316.             throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.'$property));
  317.         }
  318.         $result self::$resultProto;
  319.         $object $zval[self::VALUE];
  320.         $access $this->getReadAccessInfo(\get_class($object), $property);
  321.         if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
  322.             $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
  323.         } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
  324.             $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
  325.             if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
  326.                 $result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
  327.             }
  328.         } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property)) {
  329.             // Needed to support \stdClass instances. We need to explicitly
  330.             // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
  331.             // a *protected* property was found on the class, property_exists()
  332.             // returns true, consequently the following line will result in a
  333.             // fatal error.
  334.             $result[self::VALUE] = $object->$property;
  335.             if (isset($zval[self::REF])) {
  336.                 $result[self::REF] = &$object->$property;
  337.             }
  338.         } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
  339.             // we call the getter and hope the __call do the job
  340.             $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
  341.         } else {
  342.             throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
  343.         }
  344.         // Objects are always passed around by reference
  345.         if (isset($zval[self::REF]) && \is_object($result[self::VALUE])) {
  346.             $result[self::REF] = $result[self::VALUE];
  347.         }
  348.         return $result;
  349.     }
  350.     /**
  351.      * Guesses how to read the property value.
  352.      *
  353.      * @param string $class
  354.      * @param string $property
  355.      *
  356.      * @return array
  357.      */
  358.     private function getReadAccessInfo($class$property)
  359.     {
  360.         $key str_replace('\\''.'$class).'..'.$property;
  361.         if (isset($this->readPropertyCache[$key])) {
  362.             return $this->readPropertyCache[$key];
  363.         }
  364.         if ($this->cacheItemPool) {
  365.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.rawurlencode($key));
  366.             if ($item->isHit()) {
  367.                 return $this->readPropertyCache[$key] = $item->get();
  368.             }
  369.         }
  370.         $access = [];
  371.         $reflClass = new \ReflectionClass($class);
  372.         $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
  373.         $camelProp $this->camelize($property);
  374.         $getter 'get'.$camelProp;
  375.         $getsetter lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
  376.         $isser 'is'.$camelProp;
  377.         $hasser 'has'.$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('__get') && $reflClass->getMethod('__get')->isPublic()) {
  391.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  392.             $access[self::ACCESS_NAME] = $property;
  393.             $access[self::ACCESS_REF] = false;
  394.         } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
  395.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  396.             $access[self::ACCESS_NAME] = $property;
  397.             $access[self::ACCESS_REF] = true;
  398.         } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
  399.             // we call the getter and hope the __call do the job
  400.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
  401.             $access[self::ACCESS_NAME] = $getter;
  402.         } else {
  403.             $methods = [$getter$getsetter$isser$hasser'__get'];
  404.             if ($this->magicCall) {
  405.                 $methods[] = '__call';
  406.             }
  407.             $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  408.             $access[self::ACCESS_NAME] = sprintf(
  409.                 'Neither the property "%s" nor one of the methods "%s()" '.
  410.                 'exist and have public access in class "%s".',
  411.                 $property,
  412.                 implode('()", "'$methods),
  413.                 $reflClass->name
  414.             );
  415.         }
  416.         if (isset($item)) {
  417.             $this->cacheItemPool->save($item->set($access));
  418.         }
  419.         return $this->readPropertyCache[$key] = $access;
  420.     }
  421.     /**
  422.      * Sets the value of an index in a given array-accessible value.
  423.      *
  424.      * @param array      $zval  The array containing the array or \ArrayAccess object to write to
  425.      * @param string|int $index The index to write at
  426.      * @param mixed      $value The value to write
  427.      *
  428.      * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
  429.      */
  430.     private function writeIndex($zval$index$value)
  431.     {
  432.         if (!$zval[self::VALUE] instanceof \ArrayAccess && !\is_array($zval[self::VALUE])) {
  433.             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])));
  434.         }
  435.         $zval[self::REF][$index] = $value;
  436.     }
  437.     /**
  438.      * Sets the value of a property in the given object.
  439.      *
  440.      * @param array  $zval     The array containing the object to write to
  441.      * @param string $property The property to write
  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($zval$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.      * @param array    $zval         The array containing the object to write to
  478.      * @param string   $property     The property to write
  479.      * @param iterable $collection   The collection to write
  480.      * @param string   $addMethod    The add*() method
  481.      * @param string   $removeMethod The remove*() method
  482.      */
  483.     private function writeCollection($zval$property$collection$addMethod$removeMethod)
  484.     {
  485.         // At this point the add and remove methods have been found
  486.         $previousValue $this->readProperty($zval$property);
  487.         $previousValue $previousValue[self::VALUE];
  488.         if ($previousValue instanceof \Traversable) {
  489.             $previousValue iterator_to_array($previousValue);
  490.         }
  491.         if ($previousValue && \is_array($previousValue)) {
  492.             if (\is_object($collection)) {
  493.                 $collection iterator_to_array($collection);
  494.             }
  495.             foreach ($previousValue as $key => $item) {
  496.                 if (!\in_array($item$collectiontrue)) {
  497.                     unset($previousValue[$key]);
  498.                     $zval[self::VALUE]->{$removeMethod}($item);
  499.                 }
  500.             }
  501.         } else {
  502.             $previousValue false;
  503.         }
  504.         foreach ($collection as $item) {
  505.             if (!$previousValue || !\in_array($item$previousValuetrue)) {
  506.                 $zval[self::VALUE]->{$addMethod}($item);
  507.             }
  508.         }
  509.     }
  510.     /**
  511.      * Guesses how to write the property value.
  512.      *
  513.      * @param mixed $value
  514.      */
  515.     private function getWriteAccessInfo(string $classstring $property$value): array
  516.     {
  517.         $useAdderAndRemover = \is_array($value) || $value instanceof \Traversable;
  518.         $key str_replace('\\''.'$class).'..'.$property.'..'.(int) $useAdderAndRemover;
  519.         if (isset($this->writePropertyCache[$key])) {
  520.             return $this->writePropertyCache[$key];
  521.         }
  522.         if ($this->cacheItemPool) {
  523.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.rawurlencode($key));
  524.             if ($item->isHit()) {
  525.                 return $this->writePropertyCache[$key] = $item->get();
  526.             }
  527.         }
  528.         $access = [];
  529.         $reflClass = new \ReflectionClass($class);
  530.         $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
  531.         $camelized $this->camelize($property);
  532.         $singulars = (array) Inflector::singularize($camelized);
  533.         if ($useAdderAndRemover) {
  534.             $methods $this->findAdderAndRemover($reflClass$singulars);
  535.             if (null !== $methods) {
  536.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
  537.                 $access[self::ACCESS_ADDER] = $methods[0];
  538.                 $access[self::ACCESS_REMOVER] = $methods[1];
  539.             }
  540.         }
  541.         if (!isset($access[self::ACCESS_TYPE])) {
  542.             $setter 'set'.$camelized;
  543.             $getsetter lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
  544.             if ($this->isMethodAccessible($reflClass$setter1)) {
  545.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  546.                 $access[self::ACCESS_NAME] = $setter;
  547.             } elseif ($this->isMethodAccessible($reflClass$getsetter1)) {
  548.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  549.                 $access[self::ACCESS_NAME] = $getsetter;
  550.             } elseif ($this->isMethodAccessible($reflClass'__set'2)) {
  551.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  552.                 $access[self::ACCESS_NAME] = $property;
  553.             } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
  554.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  555.                 $access[self::ACCESS_NAME] = $property;
  556.             } elseif ($this->magicCall && $this->isMethodAccessible($reflClass'__call'2)) {
  557.                 // we call the getter and hope the __call do the job
  558.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
  559.                 $access[self::ACCESS_NAME] = $setter;
  560.             } elseif (null !== $methods $this->findAdderAndRemover($reflClass$singulars)) {
  561.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  562.                 $access[self::ACCESS_NAME] = sprintf(
  563.                     'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
  564.                     'the new value must be an array or an instance of \Traversable, '.
  565.                     '"%s" given.',
  566.                     $property,
  567.                     $reflClass->name,
  568.                     implode('()", "'$methods),
  569.                     \is_object($value) ? \get_class($value) : \gettype($value)
  570.                 );
  571.             } else {
  572.                 $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  573.                 $access[self::ACCESS_NAME] = sprintf(
  574.                     'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
  575.                     '"__set()" or "__call()" exist and have public access in class "%s".',
  576.                     $property,
  577.                     implode(''array_map(function ($singular) {
  578.                         return '"add'.$singular.'()"/"remove'.$singular.'()", ';
  579.                     }, $singulars)),
  580.                     $setter,
  581.                     $getsetter,
  582.                     $reflClass->name
  583.                 );
  584.             }
  585.         }
  586.         if (isset($item)) {
  587.             $this->cacheItemPool->save($item->set($access));
  588.         }
  589.         return $this->writePropertyCache[$key] = $access;
  590.     }
  591.     /**
  592.      * Returns whether a property is writable in the given object.
  593.      *
  594.      * @param object $object The object to write to
  595.      */
  596.     private function isPropertyWritable($objectstring $property): bool
  597.     {
  598.         if (!\is_object($object)) {
  599.             return false;
  600.         }
  601.         $access $this->getWriteAccessInfo(\get_class($object), $property, []);
  602.         $isWritable self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
  603.             || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
  604.             || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
  605.             || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property))
  606.             || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
  607.         if ($isWritable) {
  608.             return true;
  609.         }
  610.         $access $this->getWriteAccessInfo(\get_class($object), $property'');
  611.         return self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
  612.             || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
  613.             || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
  614.             || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object$property))
  615.             || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
  616.     }
  617.     /**
  618.      * Camelizes a given string.
  619.      */
  620.     private function camelize(string $string): string
  621.     {
  622.         return str_replace(' '''ucwords(str_replace('_'' '$string)));
  623.     }
  624.     /**
  625.      * Searches for add and remove methods.
  626.      *
  627.      * @param \ReflectionClass $reflClass The reflection class for the given object
  628.      * @param array            $singulars The singular form of the property name or null
  629.      *
  630.      * @return array|null An array containing the adder and remover when found, null otherwise
  631.      */
  632.     private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
  633.     {
  634.         foreach ($singulars as $singular) {
  635.             $addMethod 'add'.$singular;
  636.             $removeMethod 'remove'.$singular;
  637.             $addMethodFound $this->isMethodAccessible($reflClass$addMethod1);
  638.             $removeMethodFound $this->isMethodAccessible($reflClass$removeMethod1);
  639.             if ($addMethodFound && $removeMethodFound) {
  640.                 return [$addMethod$removeMethod];
  641.             }
  642.         }
  643.     }
  644.     /**
  645.      * Returns whether a method is public and has the number of required parameters.
  646.      */
  647.     private function isMethodAccessible(\ReflectionClass $classstring $methodNameint $parameters): bool
  648.     {
  649.         if ($class->hasMethod($methodName)) {
  650.             $method $class->getMethod($methodName);
  651.             if ($method->isPublic()
  652.                 && $method->getNumberOfRequiredParameters() <= $parameters
  653.                 && $method->getNumberOfParameters() >= $parameters) {
  654.                 return true;
  655.             }
  656.         }
  657.         return false;
  658.     }
  659.     /**
  660.      * Gets a PropertyPath instance and caches it.
  661.      *
  662.      * @param string|PropertyPath $propertyPath
  663.      */
  664.     private function getPropertyPath($propertyPath): PropertyPath
  665.     {
  666.         if ($propertyPath instanceof PropertyPathInterface) {
  667.             // Don't call the copy constructor has it is not needed here
  668.             return $propertyPath;
  669.         }
  670.         if (isset($this->propertyPathCache[$propertyPath])) {
  671.             return $this->propertyPathCache[$propertyPath];
  672.         }
  673.         if ($this->cacheItemPool) {
  674.             $item $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.rawurlencode($propertyPath));
  675.             if ($item->isHit()) {
  676.                 return $this->propertyPathCache[$propertyPath] = $item->get();
  677.             }
  678.         }
  679.         $propertyPathInstance = new PropertyPath($propertyPath);
  680.         if (isset($item)) {
  681.             $item->set($propertyPathInstance);
  682.             $this->cacheItemPool->save($item);
  683.         }
  684.         return $this->propertyPathCache[$propertyPath] = $propertyPathInstance;
  685.     }
  686.     /**
  687.      * Creates the APCu adapter if applicable.
  688.      *
  689.      * @param string               $namespace
  690.      * @param int                  $defaultLifetime
  691.      * @param string               $version
  692.      * @param LoggerInterface|null $logger
  693.      *
  694.      * @return AdapterInterface
  695.      *
  696.      * @throws \LogicException When the Cache Component isn't available
  697.      */
  698.     public static function createCache($namespace$defaultLifetime$versionLoggerInterface $logger null)
  699.     {
  700.         if (!class_exists('Symfony\Component\Cache\Adapter\ApcuAdapter')) {
  701.             throw new \LogicException(sprintf('The Symfony Cache component must be installed to use %s().'__METHOD__));
  702.         }
  703.         if (!ApcuAdapter::isSupported()) {
  704.             return new NullAdapter();
  705.         }
  706.         $apcu = new ApcuAdapter($namespace$defaultLifetime 5$version);
  707.         if ('cli' === \PHP_SAPI && !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN)) {
  708.             $apcu->setLogger(new NullLogger());
  709.         } elseif (null !== $logger) {
  710.             $apcu->setLogger($logger);
  711.         }
  712.         return $apcu;
  713.     }
  714. }