first commit

This commit is contained in:
2020-08-06 15:26:41 +08:00
commit 8c0aa3edc1
1416 changed files with 238374 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@@ -0,0 +1,40 @@
CHANGELOG
=========
4.0.0
-----
* removed the `StringUtil` class, use `Symfony\Component\Inflector\Inflector`
3.1.0
-----
* deprecated the `StringUtil` class, use `Symfony\Component\Inflector\Inflector`
instead
2.7.0
------
* `UnexpectedTypeException` now expects three constructor arguments: The invalid property value,
the `PropertyPathInterface` object and the current index of the property path.
2.5.0
------
* allowed non alpha numeric characters in second level and deeper object properties names
* [BC BREAK] when accessing an index on an object that does not implement
ArrayAccess, a NoSuchIndexException is now thrown instead of the
semantically wrong NoSuchPropertyException
* [BC BREAK] added isReadable() and isWritable() to PropertyAccessorInterface
2.3.0
------
* added PropertyAccessorBuilder, to enable or disable the support of "__call"
* added support for "__call" in the PropertyAccessor (disabled by default)
* [BC BREAK] changed PropertyAccessor to continue its search for a property or
method even if a non-public match was found. Before, a PropertyAccessDeniedException
was thrown in this case. Class PropertyAccessDeniedException was removed
now.
* deprecated PropertyAccess::getPropertyAccessor
* added PropertyAccess::createPropertyAccessor and PropertyAccess::createPropertyAccessorBuilder

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Exception;
/**
* Thrown when a property path is not available.
*
* @author Stéphane Escandell <stephane.escandell@gmail.com>
*/
class AccessException extends RuntimeException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Exception;
/**
* Marker interface for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Exception;
/**
* Base InvalidArgumentException for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Exception;
/**
* Thrown when a property path is malformed.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class InvalidPropertyPathException extends RuntimeException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Exception;
/**
* Thrown when an index cannot be found.
*
* @author Stéphane Escandell <stephane.escandell@gmail.com>
*/
class NoSuchIndexException extends AccessException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Exception;
/**
* Thrown when a property cannot be found.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NoSuchPropertyException extends AccessException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Exception;
/**
* Base OutOfBoundsException for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Exception;
/**
* Base RuntimeException for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Exception;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
/**
* Thrown when a value does not match an expected type.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UnexpectedTypeException extends RuntimeException
{
/**
* @param mixed $value The unexpected value found while traversing property path
* @param PropertyPathInterface $path The property path
* @param int $pathIndex The property path index when the unexpected value was found
*/
public function __construct($value, PropertyPathInterface $path, int $pathIndex)
{
$message = sprintf(
'PropertyAccessor requires a graph of objects or arrays to operate on, '.
'but it found type "%s" while trying to traverse path "%s" at property "%s".',
gettype($value),
(string) $path,
$path->getElement($pathIndex)
);
parent::__construct($message);
}
}

19
vendor/symfony/property-access/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-2018 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess;
/**
* Entry point of the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
final class PropertyAccess
{
/**
* Creates a property accessor with the default configuration.
*
* @return PropertyAccessor
*/
public static function createPropertyAccessor(): PropertyAccessor
{
return self::createPropertyAccessorBuilder()->getPropertyAccessor();
}
public static function createPropertyAccessorBuilder(): PropertyAccessorBuilder
{
return new PropertyAccessorBuilder();
}
/**
* This class cannot be instantiated.
*/
private function __construct()
{
}
}

View File

@@ -0,0 +1,811 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\NullAdapter;
use Symfony\Component\Inflector\Inflector;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
/**
* Default implementation of {@link PropertyAccessorInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class PropertyAccessor implements PropertyAccessorInterface
{
private const VALUE = 0;
private const REF = 1;
private const IS_REF_CHAINED = 2;
private const ACCESS_HAS_PROPERTY = 0;
private const ACCESS_TYPE = 1;
private const ACCESS_NAME = 2;
private const ACCESS_REF = 3;
private const ACCESS_ADDER = 4;
private const ACCESS_REMOVER = 5;
private const ACCESS_TYPE_METHOD = 0;
private const ACCESS_TYPE_PROPERTY = 1;
private const ACCESS_TYPE_MAGIC = 2;
private const ACCESS_TYPE_ADDER_AND_REMOVER = 3;
private const ACCESS_TYPE_NOT_FOUND = 4;
private const CACHE_PREFIX_READ = 'r';
private const CACHE_PREFIX_WRITE = 'w';
private const CACHE_PREFIX_PROPERTY_PATH = 'p';
/**
* @var bool
*/
private $magicCall;
private $ignoreInvalidIndices;
/**
* @var CacheItemPoolInterface
*/
private $cacheItemPool;
private $readPropertyCache = array();
private $writePropertyCache = array();
private static $resultProto = array(self::VALUE => null);
/**
* Should not be used by application code. Use
* {@link PropertyAccess::createPropertyAccessor()} instead.
*/
public function __construct(bool $magicCall = false, bool $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null)
{
$this->magicCall = $magicCall;
$this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex;
$this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value
}
/**
* {@inheritdoc}
*/
public function getValue($objectOrArray, $propertyPath)
{
$propertyPath = $this->getPropertyPath($propertyPath);
$zval = array(
self::VALUE => $objectOrArray,
);
$propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
return $propertyValues[count($propertyValues) - 1][self::VALUE];
}
/**
* {@inheritdoc}
*/
public function setValue(&$objectOrArray, $propertyPath, $value)
{
$propertyPath = $this->getPropertyPath($propertyPath);
$zval = array(
self::VALUE => $objectOrArray,
self::REF => &$objectOrArray,
);
$propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1);
$overwrite = true;
try {
for ($i = count($propertyValues) - 1; 0 <= $i; --$i) {
$zval = $propertyValues[$i];
unset($propertyValues[$i]);
// You only need set value for current element if:
// 1. it's the parent of the last index element
// OR
// 2. its child is not passed by reference
//
// This may avoid uncessary value setting process for array elements.
// For example:
// '[a][b][c]' => 'old-value'
// If you want to change its value to 'new-value',
// you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]'
//
if ($overwrite) {
$property = $propertyPath->getElement($i);
if ($propertyPath->isIndex($i)) {
if ($overwrite = !isset($zval[self::REF])) {
$ref = &$zval[self::REF];
$ref = $zval[self::VALUE];
}
$this->writeIndex($zval, $property, $value);
if ($overwrite) {
$zval[self::VALUE] = $zval[self::REF];
}
} else {
$this->writeProperty($zval, $property, $value);
}
// if current element is an object
// OR
// if current element's reference chain is not broken - current element
// as well as all its ancients in the property path are all passed by reference,
// then there is no need to continue the value setting process
if (is_object($zval[self::VALUE]) || isset($zval[self::IS_REF_CHAINED])) {
break;
}
}
$value = $zval[self::VALUE];
}
} catch (\TypeError $e) {
self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0);
// It wasn't thrown in this class so rethrow it
throw $e;
}
}
private static function throwInvalidArgumentException($message, $trace, $i)
{
if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file'] && isset($trace[$i]['args'][0])) {
$pos = strpos($message, $delim = 'must be of the type ') ?: (strpos($message, $delim = 'must be an instance of ') ?: strpos($message, $delim = 'must implement interface '));
$pos += strlen($delim);
$type = $trace[$i]['args'][0];
$type = is_object($type) ? get_class($type) : gettype($type);
throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given', substr($message, $pos, strpos($message, ',', $pos) - $pos), $type));
}
}
/**
* {@inheritdoc}
*/
public function isReadable($objectOrArray, $propertyPath)
{
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
}
try {
$zval = array(
self::VALUE => $objectOrArray,
);
$this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
return true;
} catch (AccessException $e) {
return false;
} catch (UnexpectedTypeException $e) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function isWritable($objectOrArray, $propertyPath)
{
$propertyPath = $this->getPropertyPath($propertyPath);
try {
$zval = array(
self::VALUE => $objectOrArray,
);
$propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1);
for ($i = count($propertyValues) - 1; 0 <= $i; --$i) {
$zval = $propertyValues[$i];
unset($propertyValues[$i]);
if ($propertyPath->isIndex($i)) {
if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
return false;
}
} else {
if (!$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) {
return false;
}
}
if (is_object($zval[self::VALUE])) {
return true;
}
}
return true;
} catch (AccessException $e) {
return false;
} catch (UnexpectedTypeException $e) {
return false;
}
}
/**
* Reads the path from an object up to a given path index.
*
* @param array $zval The array containing the object or array to read from
* @param PropertyPathInterface $propertyPath The property path to read
* @param int $lastIndex The index up to which should be read
* @param bool $ignoreInvalidIndices Whether to ignore invalid indices or throw an exception
*
* @return array The values read in the path
*
* @throws UnexpectedTypeException if a value within the path is neither object nor array
* @throws NoSuchIndexException If a non-existing index is accessed
*/
private function readPropertiesUntil($zval, PropertyPathInterface $propertyPath, $lastIndex, $ignoreInvalidIndices = true)
{
if (!is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) {
throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, 0);
}
// Add the root object to the list
$propertyValues = array($zval);
for ($i = 0; $i < $lastIndex; ++$i) {
$property = $propertyPath->getElement($i);
$isIndex = $propertyPath->isIndex($i);
if ($isIndex) {
// Create missing nested arrays on demand
if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
(is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !array_key_exists($property, $zval[self::VALUE]))
) {
if (!$ignoreInvalidIndices) {
if (!is_array($zval[self::VALUE])) {
if (!$zval[self::VALUE] instanceof \Traversable) {
throw new NoSuchIndexException(sprintf(
'Cannot read index "%s" while trying to traverse path "%s".',
$property,
(string) $propertyPath
));
}
$zval[self::VALUE] = iterator_to_array($zval[self::VALUE]);
}
throw new NoSuchIndexException(sprintf(
'Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".',
$property,
(string) $propertyPath,
print_r(array_keys($zval[self::VALUE]), true)
));
}
if ($i + 1 < $propertyPath->getLength()) {
if (isset($zval[self::REF])) {
$zval[self::VALUE][$property] = array();
$zval[self::REF] = $zval[self::VALUE];
} else {
$zval[self::VALUE] = array($property => array());
}
}
}
$zval = $this->readIndex($zval, $property);
} else {
$zval = $this->readProperty($zval, $property);
}
// the final value of the path must not be validated
if ($i + 1 < $propertyPath->getLength() && !is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) {
throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1);
}
if (isset($zval[self::REF]) && (0 === $i || isset($propertyValues[$i - 1][self::IS_REF_CHAINED]))) {
// Set the IS_REF_CHAINED flag to true if:
// current property is passed by reference and
// it is the first element in the property path or
// the IS_REF_CHAINED flag of its parent element is true
// Basically, this flag is true only when the reference chain from the top element to current element is not broken
$zval[self::IS_REF_CHAINED] = true;
}
$propertyValues[] = $zval;
}
return $propertyValues;
}
/**
* Reads a key from an array-like structure.
*
* @param array $zval The array containing the array or \ArrayAccess object to read from
* @param string|int $index The key to read
*
* @return array The array containing the value of the key
*
* @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
*/
private function readIndex($zval, $index)
{
if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
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])));
}
$result = self::$resultProto;
if (isset($zval[self::VALUE][$index])) {
$result[self::VALUE] = $zval[self::VALUE][$index];
if (!isset($zval[self::REF])) {
// Save creating references when doing read-only lookups
} elseif (is_array($zval[self::VALUE])) {
$result[self::REF] = &$zval[self::REF][$index];
} elseif (is_object($result[self::VALUE])) {
$result[self::REF] = $result[self::VALUE];
}
}
return $result;
}
/**
* Reads the a property from an object.
*
* @param array $zval The array containing the object to read from
* @param string $property The property to read
*
* @return array The array containing the value of the property
*
* @throws NoSuchPropertyException if the property does not exist or is not public
*/
private function readProperty($zval, $property)
{
if (!is_object($zval[self::VALUE])) {
throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%1$s]" instead.', $property));
}
$result = self::$resultProto;
$object = $zval[self::VALUE];
$access = $this->getReadAccessInfo(get_class($object), $property);
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
$result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
}
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly
// exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
// a *protected* property was found on the class, property_exists()
// returns true, consequently the following line will result in a
// fatal error.
$result[self::VALUE] = $object->$property;
if (isset($zval[self::REF])) {
$result[self::REF] = &$object->$property;
}
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
// we call the getter and hope the __call do the job
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
} else {
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
}
// Objects are always passed around by reference
if (isset($zval[self::REF]) && is_object($result[self::VALUE])) {
$result[self::REF] = $result[self::VALUE];
}
return $result;
}
/**
* Guesses how to read the property value.
*
* @param string $class
* @param string $property
*
* @return array
*/
private function getReadAccessInfo($class, $property)
{
$key = (false !== strpos($class, '@') ? rawurlencode($class) : $class).'..'.$property;
if (isset($this->readPropertyCache[$key])) {
return $this->readPropertyCache[$key];
}
if ($this->cacheItemPool) {
$item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.str_replace('\\', '.', $key));
if ($item->isHit()) {
return $this->readPropertyCache[$key] = $item->get();
}
}
$access = array();
$reflClass = new \ReflectionClass($class);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelProp = $this->camelize($property);
$getter = 'get'.$camelProp;
$getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
$isser = 'is'.$camelProp;
$hasser = 'has'.$camelProp;
if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $getter;
} elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $getsetter;
} elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $isser;
} elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $hasser;
} elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
$access[self::ACCESS_REF] = false;
} elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
$access[self::ACCESS_REF] = true;
} elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
// we call the getter and hope the __call do the job
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
$access[self::ACCESS_NAME] = $getter;
} else {
$methods = array($getter, $getsetter, $isser, $hasser, '__get');
if ($this->magicCall) {
$methods[] = '__call';
}
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
$access[self::ACCESS_NAME] = sprintf(
'Neither the property "%s" nor one of the methods "%s()" '.
'exist and have public access in class "%s".',
$property,
implode('()", "', $methods),
$reflClass->name
);
}
if (isset($item)) {
$this->cacheItemPool->save($item->set($access));
}
return $this->readPropertyCache[$key] = $access;
}
/**
* Sets the value of an index in a given array-accessible value.
*
* @param array $zval The array containing the array or \ArrayAccess object to write to
* @param string|int $index The index to write at
* @param mixed $value The value to write
*
* @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
*/
private function writeIndex($zval, $index, $value)
{
if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
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])));
}
$zval[self::REF][$index] = $value;
}
/**
* Sets the value of a property in the given object.
*
* @param array $zval The array containing the object to write to
* @param string $property The property to write
* @param mixed $value The value to write
*
* @throws NoSuchPropertyException if the property does not exist or is not public
*/
private function writeProperty($zval, $property, $value)
{
if (!is_object($zval[self::VALUE])) {
throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%1$s]" instead?', $property));
}
$object = $zval[self::VALUE];
$access = $this->getWriteAccessInfo(get_class($object), $property, $value);
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]}($value);
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]} = $value;
} elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) {
$this->writeCollection($zval, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]);
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly
// exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
// a *protected* property was found on the class, property_exists()
// returns true, consequently the following line will result in a
// fatal error.
$object->$property = $value;
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]}($value);
} elseif (self::ACCESS_TYPE_NOT_FOUND === $access[self::ACCESS_TYPE]) {
throw new NoSuchPropertyException(sprintf('Could not determine access type for property "%s" in class "%s".', $property, get_class($object)));
} else {
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
}
}
/**
* Adjusts a collection-valued property by calling add*() and remove*() methods.
*
* @param array $zval The array containing the object to write to
* @param string $property The property to write
* @param iterable $collection The collection to write
* @param string $addMethod The add*() method
* @param string $removeMethod The remove*() method
*/
private function writeCollection($zval, $property, $collection, $addMethod, $removeMethod)
{
// At this point the add and remove methods have been found
$previousValue = $this->readProperty($zval, $property);
$previousValue = $previousValue[self::VALUE];
if ($previousValue instanceof \Traversable) {
$previousValue = iterator_to_array($previousValue);
}
if ($previousValue && is_array($previousValue)) {
if (is_object($collection)) {
$collection = iterator_to_array($collection);
}
foreach ($previousValue as $key => $item) {
if (!in_array($item, $collection, true)) {
unset($previousValue[$key]);
$zval[self::VALUE]->{$removeMethod}($item);
}
}
} else {
$previousValue = false;
}
foreach ($collection as $item) {
if (!$previousValue || !in_array($item, $previousValue, true)) {
$zval[self::VALUE]->{$addMethod}($item);
}
}
}
/**
* Guesses how to write the property value.
*
* @param mixed $value
*/
private function getWriteAccessInfo(string $class, string $property, $value): array
{
$key = (false !== strpos($class, '@') ? rawurlencode($class) : $class).'..'.$property;
if (isset($this->writePropertyCache[$key])) {
return $this->writePropertyCache[$key];
}
if ($this->cacheItemPool) {
$item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.str_replace('\\', '.', $key));
if ($item->isHit()) {
return $this->writePropertyCache[$key] = $item->get();
}
}
$access = array();
$reflClass = new \ReflectionClass($class);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelized = $this->camelize($property);
$singulars = (array) Inflector::singularize($camelized);
if (is_array($value) || $value instanceof \Traversable) {
$methods = $this->findAdderAndRemover($reflClass, $singulars);
if (null !== $methods) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
$access[self::ACCESS_ADDER] = $methods[0];
$access[self::ACCESS_REMOVER] = $methods[1];
}
}
if (!isset($access[self::ACCESS_TYPE])) {
$setter = 'set'.$camelized;
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $setter;
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $getsetter;
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
} elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
// we call the getter and hope the __call do the job
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
$access[self::ACCESS_NAME] = $setter;
} elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
$access[self::ACCESS_NAME] = sprintf(
'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
'the new value must be an array or an instance of \Traversable, '.
'"%s" given.',
$property,
$reflClass->name,
implode('()", "', $methods),
is_object($value) ? get_class($value) : gettype($value)
);
} else {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
$access[self::ACCESS_NAME] = sprintf(
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
'"__set()" or "__call()" exist and have public access in class "%s".',
$property,
implode('', array_map(function ($singular) {
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
}, $singulars)),
$setter,
$getsetter,
$reflClass->name
);
}
}
if (isset($item)) {
$this->cacheItemPool->save($item->set($access));
}
return $this->writePropertyCache[$key] = $access;
}
/**
* Returns whether a property is writable in the given object.
*
* @param object $object The object to write to
*/
private function isPropertyWritable($object, string $property): bool
{
if (!is_object($object)) {
return false;
}
$access = $this->getWriteAccessInfo(get_class($object), $property, array());
return self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
|| self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
|| self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
|| (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property))
|| self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
}
/**
* Camelizes a given string.
*/
private function camelize(string $string): string
{
return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
}
/**
* Searches for add and remove methods.
*
* @param \ReflectionClass $reflClass The reflection class for the given object
* @param array $singulars The singular form of the property name or null
*
* @return array|null An array containing the adder and remover when found, null otherwise
*/
private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
{
foreach ($singulars as $singular) {
$addMethod = 'add'.$singular;
$removeMethod = 'remove'.$singular;
$addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1);
$removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1);
if ($addMethodFound && $removeMethodFound) {
return array($addMethod, $removeMethod);
}
}
}
/**
* Returns whether a method is public and has the number of required parameters.
*/
private function isMethodAccessible(\ReflectionClass $class, string $methodName, int $parameters): bool
{
if ($class->hasMethod($methodName)) {
$method = $class->getMethod($methodName);
if ($method->isPublic()
&& $method->getNumberOfRequiredParameters() <= $parameters
&& $method->getNumberOfParameters() >= $parameters) {
return true;
}
}
return false;
}
/**
* Gets a PropertyPath instance and caches it.
*
* @param string|PropertyPath $propertyPath
*/
private function getPropertyPath($propertyPath): PropertyPath
{
if ($propertyPath instanceof PropertyPathInterface) {
// Don't call the copy constructor has it is not needed here
return $propertyPath;
}
if (isset($this->propertyPathCache[$propertyPath])) {
return $this->propertyPathCache[$propertyPath];
}
if ($this->cacheItemPool) {
$item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.$propertyPath);
if ($item->isHit()) {
return $this->propertyPathCache[$propertyPath] = $item->get();
}
}
$propertyPathInstance = new PropertyPath($propertyPath);
if (isset($item)) {
$item->set($propertyPathInstance);
$this->cacheItemPool->save($item);
}
return $this->propertyPathCache[$propertyPath] = $propertyPathInstance;
}
/**
* Creates the APCu adapter if applicable.
*
* @param string $namespace
* @param int $defaultLifetime
* @param string $version
* @param LoggerInterface|null $logger
*
* @return AdapterInterface
*
* @throws RuntimeException When the Cache Component isn't available
*/
public static function createCache($namespace, $defaultLifetime, $version, LoggerInterface $logger = null)
{
if (!class_exists('Symfony\Component\Cache\Adapter\ApcuAdapter')) {
throw new \RuntimeException(sprintf('The Symfony Cache component must be installed to use %s().', __METHOD__));
}
if (!ApcuAdapter::isSupported()) {
return new NullAdapter();
}
$apcu = new ApcuAdapter($namespace, $defaultLifetime / 5, $version);
if ('cli' === \PHP_SAPI && !ini_get('apc.enable_cli')) {
$apcu->setLogger(new NullLogger());
} elseif (null !== $logger) {
$apcu->setLogger($logger);
}
return $apcu;
}
}

View File

@@ -0,0 +1,133 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess;
use Psr\Cache\CacheItemPoolInterface;
/**
* A configurable builder to create a PropertyAccessor.
*
* @author Jérémie Augustin <jeremie.augustin@pixel-cookers.com>
*/
class PropertyAccessorBuilder
{
private $magicCall = false;
private $throwExceptionOnInvalidIndex = false;
/**
* @var CacheItemPoolInterface|null
*/
private $cacheItemPool;
/**
* Enables the use of "__call" by the PropertyAccessor.
*
* @return $this
*/
public function enableMagicCall()
{
$this->magicCall = true;
return $this;
}
/**
* Disables the use of "__call" by the PropertyAccessor.
*
* @return $this
*/
public function disableMagicCall()
{
$this->magicCall = false;
return $this;
}
/**
* @return bool whether the use of "__call" by the PropertyAccessor is enabled
*/
public function isMagicCallEnabled()
{
return $this->magicCall;
}
/**
* Enables exceptions when reading a non-existing index.
*
* This has no influence on writing non-existing indices with PropertyAccessorInterface::setValue()
* which are always created on-the-fly.
*
* @return $this
*/
public function enableExceptionOnInvalidIndex()
{
$this->throwExceptionOnInvalidIndex = true;
return $this;
}
/**
* Disables exceptions when reading a non-existing index.
*
* Instead, null is returned when calling PropertyAccessorInterface::getValue() on a non-existing index.
*
* @return $this
*/
public function disableExceptionOnInvalidIndex()
{
$this->throwExceptionOnInvalidIndex = false;
return $this;
}
/**
* @return bool whether an exception is thrown or null is returned when reading a non-existing index
*/
public function isExceptionOnInvalidIndexEnabled()
{
return $this->throwExceptionOnInvalidIndex;
}
/**
* Sets a cache system.
*
* @param CacheItemPoolInterface|null $cacheItemPool
*
* @return PropertyAccessorBuilder The builder object
*/
public function setCacheItemPool(CacheItemPoolInterface $cacheItemPool = null)
{
$this->cacheItemPool = $cacheItemPool;
return $this;
}
/**
* Gets the used cache system.
*
* @return CacheItemPoolInterface|null
*/
public function getCacheItemPool()
{
return $this->cacheItemPool;
}
/**
* Builds and returns a new PropertyAccessor object.
*
* @return PropertyAccessorInterface The built PropertyAccessor
*/
public function getPropertyAccessor()
{
return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool);
}
}

View File

@@ -0,0 +1,114 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess;
/**
* Writes and reads values to/from an object/array graph.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface PropertyAccessorInterface
{
/**
* Sets the value at the end of the property path of the object graph.
*
* Example:
*
* use Symfony\Component\PropertyAccess\PropertyAccess;
*
* $propertyAccessor = PropertyAccess::createPropertyAccessor();
*
* echo $propertyAccessor->setValue($object, 'child.name', 'Fabien');
* // equals echo $object->getChild()->setName('Fabien');
*
* This method first tries to find a public setter for each property in the
* path. The name of the setter must be the camel-cased property name
* prefixed with "set".
*
* If the setter does not exist, this method tries to find a public
* property. The value of the property is then changed.
*
* If neither is found, an exception is thrown.
*
* @param object|array $objectOrArray The object or array to modify
* @param string|PropertyPathInterface $propertyPath The property path to modify
* @param mixed $value The value to set at the end of the property path
*
* @throws Exception\InvalidArgumentException If the property path is invalid
* @throws Exception\AccessException If a property/index does not exist or is not public
* @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array
*/
public function setValue(&$objectOrArray, $propertyPath, $value);
/**
* Returns the value at the end of the property path of the object graph.
*
* Example:
*
* use Symfony\Component\PropertyAccess\PropertyAccess;
*
* $propertyAccessor = PropertyAccess::createPropertyAccessor();
*
* echo $propertyAccessor->getValue($object, 'child.name);
* // equals echo $object->getChild()->getName();
*
* This method first tries to find a public getter for each property in the
* path. The name of the getter must be the camel-cased property name
* prefixed with "get", "is", or "has".
*
* If the getter does not exist, this method tries to find a public
* property. The value of the property is then returned.
*
* If none of them are found, an exception is thrown.
*
* @param object|array $objectOrArray The object or array to traverse
* @param string|PropertyPathInterface $propertyPath The property path to read
*
* @return mixed The value at the end of the property path
*
* @throws Exception\InvalidArgumentException If the property path is invalid
* @throws Exception\AccessException If a property/index does not exist or is not public
* @throws Exception\UnexpectedTypeException If a value within the path is neither object
* nor array
*/
public function getValue($objectOrArray, $propertyPath);
/**
* Returns whether a value can be written at a given property path.
*
* Whenever this method returns true, {@link setValue()} is guaranteed not
* to throw an exception when called with the same arguments.
*
* @param object|array $objectOrArray The object or array to check
* @param string|PropertyPathInterface $propertyPath The property path to check
*
* @return bool Whether the value can be set
*
* @throws Exception\InvalidArgumentException If the property path is invalid
*/
public function isWritable($objectOrArray, $propertyPath);
/**
* Returns whether a property path can be read from an object graph.
*
* Whenever this method returns true, {@link getValue()} is guaranteed not
* to throw an exception when called with the same arguments.
*
* @param object|array $objectOrArray The object or array to check
* @param string|PropertyPathInterface $propertyPath The property path to check
*
* @return bool Whether the property path can be read
*
* @throws Exception\InvalidArgumentException If the property path is invalid
*/
public function isReadable($objectOrArray, $propertyPath);
}

View File

@@ -0,0 +1,215 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException;
/**
* Default implementation of {@link PropertyPathInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPath implements \IteratorAggregate, PropertyPathInterface
{
/**
* Character used for separating between plural and singular of an element.
*/
const SINGULAR_SEPARATOR = '|';
/**
* The elements of the property path.
*
* @var array
*/
private $elements = array();
/**
* The number of elements in the property path.
*
* @var int
*/
private $length;
/**
* Contains a Boolean for each property in $elements denoting whether this
* element is an index. It is a property otherwise.
*
* @var array
*/
private $isIndex = array();
/**
* String representation of the path.
*
* @var string
*/
private $pathAsString;
/**
* Constructs a property path from a string.
*
* @param PropertyPath|string $propertyPath The property path as string or instance
*
* @throws InvalidArgumentException If the given path is not a string
* @throws InvalidPropertyPathException If the syntax of the property path is not valid
*/
public function __construct($propertyPath)
{
// Can be used as copy constructor
if ($propertyPath instanceof self) {
/* @var PropertyPath $propertyPath */
$this->elements = $propertyPath->elements;
$this->length = $propertyPath->length;
$this->isIndex = $propertyPath->isIndex;
$this->pathAsString = $propertyPath->pathAsString;
return;
}
if (!is_string($propertyPath)) {
throw new InvalidArgumentException(sprintf(
'The property path constructor needs a string or an instance of '.
'"Symfony\Component\PropertyAccess\PropertyPath". '.
'Got: "%s"',
is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath)
));
}
if ('' === $propertyPath) {
throw new InvalidPropertyPathException('The property path should not be empty.');
}
$this->pathAsString = $propertyPath;
$position = 0;
$remaining = $propertyPath;
// first element is evaluated differently - no leading dot for properties
$pattern = '/^(([^\.\[]++)|\[([^\]]++)\])(.*)/';
while (preg_match($pattern, $remaining, $matches)) {
if ('' !== $matches[2]) {
$element = $matches[2];
$this->isIndex[] = false;
} else {
$element = $matches[3];
$this->isIndex[] = true;
}
$this->elements[] = $element;
$position += strlen($matches[1]);
$remaining = $matches[4];
$pattern = '/^(\.([^\.|\[]++)|\[([^\]]++)\])(.*)/';
}
if ('' !== $remaining) {
throw new InvalidPropertyPathException(sprintf(
'Could not parse property path "%s". Unexpected token "%s" at position %d',
$propertyPath,
$remaining[0],
$position
));
}
$this->length = count($this->elements);
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->pathAsString;
}
/**
* {@inheritdoc}
*/
public function getLength()
{
return $this->length;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
if ($this->length <= 1) {
return;
}
$parent = clone $this;
--$parent->length;
$parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '[')));
array_pop($parent->elements);
array_pop($parent->isIndex);
return $parent;
}
/**
* Returns a new iterator for this path.
*
* @return PropertyPathIteratorInterface
*/
public function getIterator()
{
return new PropertyPathIterator($this);
}
/**
* {@inheritdoc}
*/
public function getElements()
{
return $this->elements;
}
/**
* {@inheritdoc}
*/
public function getElement($index)
{
if (!isset($this->elements[$index])) {
throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index));
}
return $this->elements[$index];
}
/**
* {@inheritdoc}
*/
public function isProperty($index)
{
if (!isset($this->isIndex[$index])) {
throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index));
}
return !$this->isIndex[$index];
}
/**
* {@inheritdoc}
*/
public function isIndex($index)
{
if (!isset($this->isIndex[$index])) {
throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index));
}
return $this->isIndex[$index];
}
}

View File

@@ -0,0 +1,299 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathBuilder
{
private $elements = array();
private $isIndex = array();
/**
* Creates a new property path builder.
*
* @param null|PropertyPathInterface|string $path The path to initially store
* in the builder. Optional.
*/
public function __construct($path = null)
{
if (null !== $path) {
$this->append($path);
}
}
/**
* Appends a (sub-) path to the current path.
*
* @param PropertyPathInterface|string $path The path to append
* @param int $offset The offset where the appended
* piece starts in $path
* @param int $length The length of the appended piece
* If 0, the full path is appended
*/
public function append($path, $offset = 0, $length = 0)
{
if (is_string($path)) {
$path = new PropertyPath($path);
}
if (0 === $length) {
$end = $path->getLength();
} else {
$end = $offset + $length;
}
for (; $offset < $end; ++$offset) {
$this->elements[] = $path->getElement($offset);
$this->isIndex[] = $path->isIndex($offset);
}
}
/**
* Appends an index element to the current path.
*
* @param string $name The name of the appended index
*/
public function appendIndex($name)
{
$this->elements[] = $name;
$this->isIndex[] = true;
}
/**
* Appends a property element to the current path.
*
* @param string $name The name of the appended property
*/
public function appendProperty($name)
{
$this->elements[] = $name;
$this->isIndex[] = false;
}
/**
* Removes elements from the current path.
*
* @param int $offset The offset at which to remove
* @param int $length The length of the removed piece
*
* @throws OutOfBoundsException if offset is invalid
*/
public function remove($offset, $length = 1)
{
if (!isset($this->elements[$offset])) {
throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
}
$this->resize($offset, $length, 0);
}
/**
* Replaces a sub-path by a different (sub-) path.
*
* @param int $offset The offset at which to replace
* @param int $length The length of the piece to replace
* @param PropertyPathInterface|string $path The path to insert
* @param int $pathOffset The offset where the inserted piece
* starts in $path
* @param int $pathLength The length of the inserted piece
* If 0, the full path is inserted
*
* @throws OutOfBoundsException If the offset is invalid
*/
public function replace($offset, $length, $path, $pathOffset = 0, $pathLength = 0)
{
if (is_string($path)) {
$path = new PropertyPath($path);
}
if ($offset < 0 && abs($offset) <= $this->getLength()) {
$offset = $this->getLength() + $offset;
} elseif (!isset($this->elements[$offset])) {
throw new OutOfBoundsException('The offset '.$offset.' is not within the property path');
}
if (0 === $pathLength) {
$pathLength = $path->getLength() - $pathOffset;
}
$this->resize($offset, $length, $pathLength);
for ($i = 0; $i < $pathLength; ++$i) {
$this->elements[$offset + $i] = $path->getElement($pathOffset + $i);
$this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i);
}
ksort($this->elements);
}
/**
* Replaces a property element by an index element.
*
* @param int $offset The offset at which to replace
* @param string $name The new name of the element. Optional
*
* @throws OutOfBoundsException If the offset is invalid
*/
public function replaceByIndex($offset, $name = null)
{
if (!isset($this->elements[$offset])) {
throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
}
if (null !== $name) {
$this->elements[$offset] = $name;
}
$this->isIndex[$offset] = true;
}
/**
* Replaces an index element by a property element.
*
* @param int $offset The offset at which to replace
* @param string $name The new name of the element. Optional
*
* @throws OutOfBoundsException If the offset is invalid
*/
public function replaceByProperty($offset, $name = null)
{
if (!isset($this->elements[$offset])) {
throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
}
if (null !== $name) {
$this->elements[$offset] = $name;
}
$this->isIndex[$offset] = false;
}
/**
* Returns the length of the current path.
*
* @return int The path length
*/
public function getLength()
{
return count($this->elements);
}
/**
* Returns the current property path.
*
* @return PropertyPathInterface The constructed property path
*/
public function getPropertyPath()
{
$pathAsString = $this->__toString();
return '' !== $pathAsString ? new PropertyPath($pathAsString) : null;
}
/**
* Returns the current property path as string.
*
* @return string The property path as string
*/
public function __toString()
{
$string = '';
foreach ($this->elements as $offset => $element) {
if ($this->isIndex[$offset]) {
$element = '['.$element.']';
} elseif ('' !== $string) {
$string .= '.';
}
$string .= $element;
}
return $string;
}
/**
* Resizes the path so that a chunk of length $cutLength is
* removed at $offset and another chunk of length $insertionLength
* can be inserted.
*
* @param int $offset The offset where the removed chunk starts
* @param int $cutLength The length of the removed chunk
* @param int $insertionLength The length of the inserted chunk
*/
private function resize($offset, $cutLength, $insertionLength)
{
// Nothing else to do in this case
if ($insertionLength === $cutLength) {
return;
}
$length = count($this->elements);
if ($cutLength > $insertionLength) {
// More elements should be removed than inserted
$diff = $cutLength - $insertionLength;
$newLength = $length - $diff;
// Shift elements to the left (left-to-right until the new end)
// Max allowed offset to be shifted is such that
// $offset + $diff < $length (otherwise invalid index access)
// i.e. $offset < $length - $diff = $newLength
for ($i = $offset; $i < $newLength; ++$i) {
$this->elements[$i] = $this->elements[$i + $diff];
$this->isIndex[$i] = $this->isIndex[$i + $diff];
}
// All remaining elements should be removed
for (; $i < $length; ++$i) {
unset($this->elements[$i], $this->isIndex[$i]);
}
} else {
$diff = $insertionLength - $cutLength;
$newLength = $length + $diff;
$indexAfterInsertion = $offset + $insertionLength;
// $diff <= $insertionLength
// $indexAfterInsertion >= $insertionLength
// => $diff <= $indexAfterInsertion
// In each of the following loops, $i >= $diff must hold,
// otherwise ($i - $diff) becomes negative.
// Shift old elements to the right to make up space for the
// inserted elements. This needs to be done left-to-right in
// order to preserve an ascending array index order
// Since $i = max($length, $indexAfterInsertion) and $indexAfterInsertion >= $diff,
// $i >= $diff is guaranteed.
for ($i = max($length, $indexAfterInsertion); $i < $newLength; ++$i) {
$this->elements[$i] = $this->elements[$i - $diff];
$this->isIndex[$i] = $this->isIndex[$i - $diff];
}
// Shift remaining elements to the right. Do this right-to-left
// so we don't overwrite elements before copying them
// The last written index is the immediate index after the inserted
// string, because the indices before that will be overwritten
// anyway.
// Since $i >= $indexAfterInsertion and $indexAfterInsertion >= $diff,
// $i >= $diff is guaranteed.
for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) {
$this->elements[$i] = $this->elements[$i - $diff];
$this->isIndex[$i] = $this->isIndex[$i - $diff];
}
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess;
/**
* A sequence of property names or array indices.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface PropertyPathInterface extends \Traversable
{
/**
* Returns the string representation of the property path.
*
* @return string The path as string
*/
public function __toString();
/**
* Returns the length of the property path, i.e. the number of elements.
*
* @return int The path length
*/
public function getLength();
/**
* Returns the parent property path.
*
* The parent property path is the one that contains the same items as
* this one except for the last one.
*
* If this property path only contains one item, null is returned.
*
* @return PropertyPath The parent path or null
*/
public function getParent();
/**
* Returns the elements of the property path as array.
*
* @return array An array of property/index names
*/
public function getElements();
/**
* Returns the element at the given index in the property path.
*
* @param int $index The index key
*
* @return string A property or index name
*
* @throws Exception\OutOfBoundsException If the offset is invalid
*/
public function getElement($index);
/**
* Returns whether the element at the given index is a property.
*
* @param int $index The index in the property path
*
* @return bool Whether the element at this index is a property
*
* @throws Exception\OutOfBoundsException If the offset is invalid
*/
public function isProperty($index);
/**
* Returns whether the element at the given index is an array index.
*
* @param int $index The index in the property path
*
* @return bool Whether the element at this index is an array index
*
* @throws Exception\OutOfBoundsException If the offset is invalid
*/
public function isIndex($index);
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess;
/**
* Traverses a property path and provides additional methods to find out
* information about the current element.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface
{
protected $path;
/**
* @param PropertyPathInterface $path The property path to traverse
*/
public function __construct(PropertyPathInterface $path)
{
parent::__construct($path->getElements());
$this->path = $path;
}
/**
* {@inheritdoc}
*/
public function isIndex()
{
return $this->path->isIndex($this->key());
}
/**
* {@inheritdoc}
*/
public function isProperty()
{
return $this->path->isProperty($this->key());
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface PropertyPathIteratorInterface extends \Iterator, \SeekableIterator
{
/**
* Returns whether the current element in the property path is an array
* index.
*
* @return bool
*/
public function isIndex();
/**
* Returns whether the current element in the property path is a property
* name.
*
* @return bool
*/
public function isProperty();
}

View File

@@ -0,0 +1,14 @@
PropertyAccess Component
========================
The PropertyAccess component provides function to read and write from/to an
object or array using a simple string notation.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/property_access/index.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
/**
* This class is a hand written simplified version of PHP native `ArrayObject`
* class, to show that it behaves differently than the PHP native implementation.
*/
class NonTraversableArrayObject implements \ArrayAccess, \Countable, \Serializable
{
private $array;
public function __construct(array $array = null)
{
$this->array = $array ?: array();
}
public function offsetExists($offset)
{
return array_key_exists($offset, $this->array);
}
public function offsetGet($offset)
{
return $this->array[$offset];
}
public function offsetSet($offset, $value)
{
if (null === $offset) {
$this->array[] = $value;
} else {
$this->array[$offset] = $value;
}
}
public function offsetUnset($offset)
{
unset($this->array[$offset]);
}
public function count()
{
return count($this->array);
}
public function serialize()
{
return serialize($this->array);
}
public function unserialize($serialized)
{
$this->array = (array) unserialize((string) $serialized);
}
}

View File

@@ -0,0 +1,187 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
class TestClass
{
public $publicProperty;
protected $protectedProperty;
private $privateProperty;
private $publicAccessor;
private $publicMethodAccessor;
private $publicGetSetter;
private $publicAccessorWithDefaultValue;
private $publicAccessorWithRequiredAndDefaultValue;
private $publicAccessorWithMoreRequiredParameters;
private $publicIsAccessor;
private $publicHasAccessor;
private $publicGetter;
private $date;
public function __construct($value)
{
$this->publicProperty = $value;
$this->publicAccessor = $value;
$this->publicMethodAccessor = $value;
$this->publicGetSetter = $value;
$this->publicAccessorWithDefaultValue = $value;
$this->publicAccessorWithRequiredAndDefaultValue = $value;
$this->publicAccessorWithMoreRequiredParameters = $value;
$this->publicIsAccessor = $value;
$this->publicHasAccessor = $value;
$this->publicGetter = $value;
}
public function setPublicAccessor($value)
{
$this->publicAccessor = $value;
}
public function setPublicAccessorWithDefaultValue($value = null)
{
$this->publicAccessorWithDefaultValue = $value;
}
public function setPublicAccessorWithRequiredAndDefaultValue($value, $optional = null)
{
$this->publicAccessorWithRequiredAndDefaultValue = $value;
}
public function setPublicAccessorWithMoreRequiredParameters($value, $needed)
{
$this->publicAccessorWithMoreRequiredParameters = $value;
}
public function getPublicAccessor()
{
return $this->publicAccessor;
}
public function getPublicAccessorWithDefaultValue()
{
return $this->publicAccessorWithDefaultValue;
}
public function getPublicAccessorWithRequiredAndDefaultValue()
{
return $this->publicAccessorWithRequiredAndDefaultValue;
}
public function getPublicAccessorWithMoreRequiredParameters()
{
return $this->publicAccessorWithMoreRequiredParameters;
}
public function setPublicIsAccessor($value)
{
$this->publicIsAccessor = $value;
}
public function isPublicIsAccessor()
{
return $this->publicIsAccessor;
}
public function setPublicHasAccessor($value)
{
$this->publicHasAccessor = $value;
}
public function hasPublicHasAccessor()
{
return $this->publicHasAccessor;
}
public function publicGetSetter($value = null)
{
if (null !== $value) {
$this->publicGetSetter = $value;
}
return $this->publicGetSetter;
}
public function getPublicMethodMutator()
{
return $this->publicGetSetter;
}
protected function setProtectedAccessor($value)
{
}
protected function getProtectedAccessor()
{
return 'foobar';
}
protected function setProtectedIsAccessor($value)
{
}
protected function isProtectedIsAccessor()
{
return 'foobar';
}
protected function setProtectedHasAccessor($value)
{
}
protected function hasProtectedHasAccessor()
{
return 'foobar';
}
private function setPrivateAccessor($value)
{
}
private function getPrivateAccessor()
{
return 'foobar';
}
private function setPrivateIsAccessor($value)
{
}
private function isPrivateIsAccessor()
{
return 'foobar';
}
private function setPrivateHasAccessor($value)
{
}
private function hasPrivateHasAccessor()
{
return 'foobar';
}
public function getPublicGetter()
{
return $this->publicGetter;
}
public function setDate(\DateTimeInterface $date)
{
$this->date = $date;
}
public function getDate()
{
return $this->date;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
class TestClassIsWritable
{
protected $value;
public function getValue()
{
return $this->value;
}
public function __construct($value)
{
$this->value = $value;
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
class TestClassMagicCall
{
private $magicCallProperty;
public function __construct($value)
{
$this->magicCallProperty = $value;
}
public function __call($method, array $args)
{
if ('getMagicCallProperty' === $method) {
return $this->magicCallProperty;
}
if ('getConstantMagicCallProperty' === $method) {
return 'constant value';
}
if ('setMagicCallProperty' === $method) {
$this->magicCallProperty = reset($args);
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
class TestClassMagicGet
{
private $magicProperty;
public $publicProperty;
public function __construct($value)
{
$this->magicProperty = $value;
}
public function __set($property, $value)
{
if ('magicProperty' === $property) {
$this->magicProperty = $value;
}
}
public function __get($property)
{
if ('magicProperty' === $property) {
return $this->magicProperty;
}
if ('constantMagicProperty' === $property) {
return 'constant value';
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
class TestClassSetValue
{
protected $value;
public function getValue()
{
return $this->value;
}
public function setValue($value)
{
$this->value = $value;
}
public function __construct($value)
{
$this->value = $value;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
class Ticket5775Object
{
private $property;
public function getProperty()
{
return $this->property;
}
private function setProperty()
{
}
public function __set($property, $value)
{
$this->$property = $value;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
/**
* This class is a hand written simplified version of PHP native `ArrayObject`
* class, to show that it behaves differently than the PHP native implementation.
*/
class TraversableArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable, \Serializable
{
private $array;
public function __construct(array $array = null)
{
$this->array = $array ?: array();
}
public function offsetExists($offset)
{
return array_key_exists($offset, $this->array);
}
public function offsetGet($offset)
{
return $this->array[$offset];
}
public function offsetSet($offset, $value)
{
if (null === $offset) {
$this->array[] = $value;
} else {
$this->array[$offset] = $value;
}
}
public function offsetUnset($offset)
{
unset($this->array[$offset]);
}
public function getIterator()
{
return new \ArrayIterator($this->array);
}
public function count()
{
return count($this->array);
}
public function serialize()
{
return serialize($this->array);
}
public function unserialize($serialized)
{
$this->array = (array) unserialize((string) $serialized);
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class TypeHinted
{
private $date;
/**
* @var \Countable
*/
private $countable;
public function setDate(\DateTime $date)
{
$this->date = $date;
}
public function getDate()
{
return $this->date;
}
/**
* @return \Countable
*/
public function getCountable()
{
return $this->countable;
}
/**
* @param \Countable $countable
*/
public function setCountable(\Countable $countable)
{
$this->countable = $countable;
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
abstract class PropertyAccessorArrayAccessTest extends TestCase
{
/**
* @var PropertyAccessor
*/
protected $propertyAccessor;
protected function setUp()
{
$this->propertyAccessor = new PropertyAccessor();
}
abstract protected function getContainer(array $array);
public function getValidPropertyPaths()
{
return array(
array($this->getContainer(array('firstName' => 'Bernhard')), '[firstName]', 'Bernhard'),
array($this->getContainer(array('person' => $this->getContainer(array('firstName' => 'Bernhard')))), '[person][firstName]', 'Bernhard'),
);
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testGetValue($collection, $path, $value)
{
$this->assertSame($value, $this->propertyAccessor->getValue($collection, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testGetValueFailsIfNoSuchIndex()
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
->enableExceptionOnInvalidIndex()
->getPropertyAccessor();
$object = $this->getContainer(array('firstName' => 'Bernhard'));
$this->propertyAccessor->getValue($object, '[lastName]');
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testSetValue($collection, $path)
{
$this->propertyAccessor->setValue($collection, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($collection, $path));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsReadable($collection, $path)
{
$this->assertTrue($this->propertyAccessor->isReadable($collection, $path));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsWritable($collection, $path)
{
$this->assertTrue($this->propertyAccessor->isWritable($collection, $path));
}
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
class PropertyAccessorArrayObjectTest extends PropertyAccessorCollectionTest
{
protected function getContainer(array $array)
{
return new \ArrayObject($array);
}
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
class PropertyAccessorArrayTest extends PropertyAccessorCollectionTest
{
protected function getContainer(array $array)
{
return $array;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\PropertyAccessorBuilder;
class PropertyAccessorBuilderTest extends TestCase
{
/**
* @var PropertyAccessorBuilder
*/
protected $builder;
protected function setUp()
{
$this->builder = new PropertyAccessorBuilder();
}
protected function tearDown()
{
$this->builder = null;
}
public function testEnableMagicCall()
{
$this->assertSame($this->builder, $this->builder->enableMagicCall());
}
public function testDisableMagicCall()
{
$this->assertSame($this->builder, $this->builder->disableMagicCall());
}
public function testIsMagicCallEnable()
{
$this->assertFalse($this->builder->isMagicCallEnabled());
$this->assertTrue($this->builder->enableMagicCall()->isMagicCallEnabled());
$this->assertFalse($this->builder->disableMagicCall()->isMagicCallEnabled());
}
public function testGetPropertyAccessor()
{
$this->assertInstanceOf(PropertyAccessor::class, $this->builder->getPropertyAccessor());
$this->assertInstanceOf(PropertyAccessor::class, $this->builder->enableMagicCall()->getPropertyAccessor());
}
public function testUseCache()
{
$cacheItemPool = new ArrayAdapter();
$this->builder->setCacheItemPool($cacheItemPool);
$this->assertEquals($cacheItemPool, $this->builder->getCacheItemPool());
$this->assertInstanceOf(PropertyAccessor::class, $this->builder->getPropertyAccessor());
}
}

View File

@@ -0,0 +1,200 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
class PropertyAccessorCollectionTest_Car
{
private $axes;
public function __construct($axes = null)
{
$this->axes = $axes;
}
// In the test, use a name that StringUtil can't uniquely singularify
public function addAxis($axis)
{
$this->axes[] = $axis;
}
public function removeAxis($axis)
{
foreach ($this->axes as $key => $value) {
if ($value === $axis) {
unset($this->axes[$key]);
return;
}
}
}
public function getAxes()
{
return $this->axes;
}
}
class PropertyAccessorCollectionTest_CarOnlyAdder
{
public function addAxis($axis)
{
}
public function getAxes()
{
}
}
class PropertyAccessorCollectionTest_CarOnlyRemover
{
public function removeAxis($axis)
{
}
public function getAxes()
{
}
}
class PropertyAccessorCollectionTest_CarNoAdderAndRemover
{
public function getAxes()
{
}
}
class PropertyAccessorCollectionTest_CompositeCar
{
public function getStructure()
{
}
public function setStructure($structure)
{
}
}
class PropertyAccessorCollectionTest_CarStructure
{
public function addAxis($axis)
{
}
public function removeAxis($axis)
{
}
public function getAxes()
{
}
}
abstract class PropertyAccessorCollectionTest extends PropertyAccessorArrayAccessTest
{
public function testSetValueCallsAdderAndRemoverForCollections()
{
$axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth', 4 => 'fifth'));
$axesMerged = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third'));
$axesAfter = $this->getContainer(array(1 => 'second', 5 => 'first', 6 => 'third'));
$axesMergedCopy = is_object($axesMerged) ? clone $axesMerged : $axesMerged;
// Don't use a mock in order to test whether the collections are
// modified while iterating them
$car = new PropertyAccessorCollectionTest_Car($axesBefore);
$this->propertyAccessor->setValue($car, 'axes', $axesMerged);
$this->assertEquals($axesAfter, $car->getAxes());
// The passed collection was not modified
$this->assertEquals($axesMergedCopy, $axesMerged);
}
public function testSetValueCallsAdderAndRemoverForNestedCollections()
{
$car = $this->getMockBuilder(__CLASS__.'_CompositeCar')->getMock();
$structure = $this->getMockBuilder(__CLASS__.'_CarStructure')->getMock();
$axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third'));
$car->expects($this->any())
->method('getStructure')
->will($this->returnValue($structure));
$structure->expects($this->at(0))
->method('getAxes')
->will($this->returnValue($axesBefore));
$structure->expects($this->at(1))
->method('removeAxis')
->with('fourth');
$structure->expects($this->at(2))
->method('addAxis')
->with('first');
$structure->expects($this->at(3))
->method('addAxis')
->with('third');
$this->propertyAccessor->setValue($car, 'structure.axes', $axesAfter);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* @expectedExceptionMessageRegExp /Could not determine access type for property "axes" in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover_[^"]*"./
*/
public function testSetValueFailsIfNoAdderNorRemoverFound()
{
$car = $this->getMockBuilder(__CLASS__.'_CarNoAdderAndRemover')->getMock();
$axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third'));
$car->expects($this->any())
->method('getAxes')
->will($this->returnValue($axesBefore));
$this->propertyAccessor->setValue($car, 'axes', $axesAfter);
}
public function testIsWritableReturnsTrueIfAdderAndRemoverExists()
{
$car = $this->getMockBuilder(__CLASS__.'_Car')->getMock();
$this->assertTrue($this->propertyAccessor->isWritable($car, 'axes'));
}
public function testIsWritableReturnsFalseIfOnlyAdderExists()
{
$car = $this->getMockBuilder(__CLASS__.'_CarOnlyAdder')->getMock();
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes'));
}
public function testIsWritableReturnsFalseIfOnlyRemoverExists()
{
$car = $this->getMockBuilder(__CLASS__.'_CarOnlyRemover')->getMock();
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes'));
}
public function testIsWritableReturnsFalseIfNoAdderNorRemoverExists()
{
$car = $this->getMockBuilder(__CLASS__.'_CarNoAdderAndRemover')->getMock();
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* expectedExceptionMessageRegExp /The property "axes" in class "Mock_PropertyAccessorCollectionTest_Car[^"]*" can be defined with the methods "addAxis()", "removeAxis()" but the new value must be an array or an instance of \Traversable, "string" given./
*/
public function testSetValueFailsIfAdderAndRemoverExistButValueIsNotTraversable()
{
$car = $this->getMockBuilder(__CLASS__.'_Car')->getMock();
$this->propertyAccessor->setValue($car, 'axes', 'Not an array or Traversable');
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\PropertyAccess\Tests\Fixtures\NonTraversableArrayObject;
class PropertyAccessorNonTraversableArrayObjectTest extends PropertyAccessorArrayAccessTest
{
protected function getContainer(array $array)
{
return new NonTraversableArrayObject($array);
}
}

View File

@@ -0,0 +1,635 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet;
use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassSetValue;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted;
class PropertyAccessorTest extends TestCase
{
/**
* @var PropertyAccessor
*/
private $propertyAccessor;
protected function setUp()
{
$this->propertyAccessor = new PropertyAccessor();
}
public function getPathsWithUnexpectedType()
{
return array(
array('', 'foobar'),
array('foo', 'foobar'),
array(null, 'foobar'),
array(123, 'foobar'),
array((object) array('prop' => null), 'prop.foobar'),
array((object) array('prop' => (object) array('subProp' => null)), 'prop.subProp.foobar'),
array(array('index' => null), '[index][foobar]'),
array(array('index' => array('subIndex' => null)), '[index][subIndex][foobar]'),
);
}
public function getPathsWithMissingProperty()
{
return array(
array((object) array('firstName' => 'Bernhard'), 'lastName'),
array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.lastName'),
array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].lastName'),
array(new TestClass('Bernhard'), 'protectedProperty'),
array(new TestClass('Bernhard'), 'privateProperty'),
array(new TestClass('Bernhard'), 'protectedAccessor'),
array(new TestClass('Bernhard'), 'protectedIsAccessor'),
array(new TestClass('Bernhard'), 'protectedHasAccessor'),
array(new TestClass('Bernhard'), 'privateAccessor'),
array(new TestClass('Bernhard'), 'privateIsAccessor'),
array(new TestClass('Bernhard'), 'privateHasAccessor'),
// Properties are not camelized
array(new TestClass('Bernhard'), 'public_property'),
);
}
public function getPathsWithMissingIndex()
{
return array(
array(array('firstName' => 'Bernhard'), '[lastName]'),
array(array(), '[index][lastName]'),
array(array('index' => array()), '[index][lastName]'),
array(array('index' => array('firstName' => 'Bernhard')), '[index][lastName]'),
array((object) array('property' => array('firstName' => 'Bernhard')), 'property[lastName]'),
);
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testGetValue($objectOrArray, $path, $value)
{
$this->assertSame($value, $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingProperty
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testGetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path)
{
$this->propertyAccessor->getValue($objectOrArray, $path);
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testGetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path)
{
$this->assertNull($this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor->getValue($objectOrArray, $path);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testGetValueThrowsExceptionIfNotArrayAccess()
{
$this->propertyAccessor->getValue(new \stdClass(), '[index]');
}
public function testGetValueReadsMagicGet()
{
$this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'magicProperty'));
}
public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath()
{
$object = new \ArrayObject();
$array = array('child' => array('index' => $object));
$this->assertNull($this->propertyAccessor->getValue($array, '[child][index][foo][bar]'));
$this->assertSame(array(), $object->getArrayCopy());
}
// https://github.com/symfony/symfony/pull/4450
public function testGetValueReadsMagicGetThatReturnsConstant()
{
$this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'constantMagicProperty'));
}
public function testGetValueNotModifyObject()
{
$object = new \stdClass();
$object->firstName = array('Bernhard');
$this->assertNull($this->propertyAccessor->getValue($object, 'firstName[1]'));
$this->assertSame(array('Bernhard'), $object->firstName);
}
public function testGetValueNotModifyObjectException()
{
$propertyAccessor = new PropertyAccessor(false, true);
$object = new \stdClass();
$object->firstName = array('Bernhard');
try {
$propertyAccessor->getValue($object, 'firstName[1]');
} catch (NoSuchIndexException $e) {
}
$this->assertSame(array('Bernhard'), $object->firstName);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testGetValueDoesNotReadMagicCallByDefault()
{
$this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty');
}
public function testGetValueReadsMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
// https://github.com/symfony/symfony/pull/4450
public function testGetValueReadsMagicCallThatReturnsConstant()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'constantMagicCallProperty'));
}
/**
* @dataProvider getPathsWithUnexpectedType
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on
*/
public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $path)
{
$this->propertyAccessor->getValue($objectOrArray, $path);
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testSetValue($objectOrArray, $path)
{
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingProperty
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path)
{
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testSetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path)
{
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testSetValueThrowsExceptionIfNotArrayAccess()
{
$object = new \stdClass();
$this->propertyAccessor->setValue($object, '[index]', 'Updated');
}
public function testSetValueUpdatesMagicSet()
{
$author = new TestClassMagicGet('Bernhard');
$this->propertyAccessor->setValue($author, 'magicProperty', 'Updated');
$this->assertEquals('Updated', $author->__get('magicProperty'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueThrowsExceptionIfThereAreMissingParameters()
{
$object = new TestClass('Bernhard');
$this->propertyAccessor->setValue($object, 'publicAccessorWithMoreRequiredParameters', 'Updated');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueDoesNotUpdateMagicCallByDefault()
{
$author = new TestClassMagicCall('Bernhard');
$this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated');
}
public function testSetValueUpdatesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$author = new TestClassMagicCall('Bernhard');
$this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated');
$this->assertEquals('Updated', $author->__call('getMagicCallProperty', array()));
}
/**
* @dataProvider getPathsWithUnexpectedType
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
* @expectedExceptionMessage PropertyAccessor requires a graph of objects or arrays to operate on
*/
public function testSetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $path)
{
$this->propertyAccessor->setValue($objectOrArray, $path, 'value');
}
public function testGetValueWhenArrayValueIsNull()
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->assertNull($this->propertyAccessor->getValue(array('index' => array('nullable' => null)), '[index][nullable]'));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsReadable($objectOrArray, $path)
{
$this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingProperty
*/
public function testIsReadableReturnsFalseIfPropertyNotFound($objectOrArray, $path)
{
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testIsReadableReturnsTrueIfIndexNotFound($objectOrArray, $path)
{
// Non-existing indices can be read. In this case, null is returned
$this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testIsReadableReturnsFalseIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
// When exceptions are enabled, non-existing indices cannot be read
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
}
public function testIsReadableRecognizesMagicGet()
{
$this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicGet('Bernhard'), 'magicProperty'));
}
public function testIsReadableDoesNotRecognizeMagicCallByDefault()
{
$this->assertFalse($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testIsReadableRecognizesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
/**
* @dataProvider getPathsWithUnexpectedType
*/
public function testIsReadableReturnsFalseIfNotObjectOrArray($objectOrArray, $path)
{
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsWritable($objectOrArray, $path)
{
$this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingProperty
*/
public function testIsWritableReturnsFalseIfPropertyNotFound($objectOrArray, $path)
{
$this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testIsWritableReturnsTrueIfIndexNotFound($objectOrArray, $path)
{
// Non-existing indices can be written. Arrays are created on-demand.
$this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testIsWritableReturnsTrueIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
// Non-existing indices can be written even if exceptions are enabled
$this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path));
}
public function testIsWritableRecognizesMagicSet()
{
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicGet('Bernhard'), 'magicProperty'));
}
public function testIsWritableDoesNotRecognizeMagicCallByDefault()
{
$this->assertFalse($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
public function testIsWritableRecognizesMagicCallIfEnabled()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
/**
* @dataProvider getPathsWithUnexpectedType
*/
public function testIsWritableReturnsFalseIfNotObjectOrArray($objectOrArray, $path)
{
$this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path));
}
public function getValidPropertyPaths()
{
return array(
array(array('Bernhard', 'Schussek'), '[0]', 'Bernhard'),
array(array('Bernhard', 'Schussek'), '[1]', 'Schussek'),
array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'),
array(array('index' => array('firstName' => 'Bernhard')), '[index][firstName]', 'Bernhard'),
array((object) array('firstName' => 'Bernhard'), 'firstName', 'Bernhard'),
array((object) array('property' => array('firstName' => 'Bernhard')), 'property[firstName]', 'Bernhard'),
array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].firstName', 'Bernhard'),
array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.firstName', 'Bernhard'),
// Accessor methods
array(new TestClass('Bernhard'), 'publicProperty', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicAccessorWithDefaultValue', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicAccessorWithRequiredAndDefaultValue', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicGetSetter', 'Bernhard'),
// Methods are camelized
array(new TestClass('Bernhard'), 'public_accessor', 'Bernhard'),
array(new TestClass('Bernhard'), '_public_accessor', 'Bernhard'),
// Missing indices
array(array('index' => array()), '[index][firstName]', null),
array(array('root' => array('index' => array())), '[root][index][firstName]', null),
// Special chars
array(array('%!@$§.' => 'Bernhard'), '[%!@$§.]', 'Bernhard'),
array(array('index' => array('%!@$§.' => 'Bernhard')), '[index][%!@$§.]', 'Bernhard'),
array((object) array('%!@$§' => 'Bernhard'), '%!@$§', 'Bernhard'),
array((object) array('property' => (object) array('%!@$§' => 'Bernhard')), 'property.%!@$§', 'Bernhard'),
// nested objects and arrays
array(array('foo' => new TestClass('bar')), '[foo].publicGetSetter', 'bar'),
array(new TestClass(array('foo' => 'bar')), 'publicGetSetter[foo]', 'bar'),
array(new TestClass(new TestClass('bar')), 'publicGetter.publicGetSetter', 'bar'),
array(new TestClass(array('foo' => new TestClass('bar'))), 'publicGetter[foo].publicGetSetter', 'bar'),
array(new TestClass(new TestClass(new TestClass('bar'))), 'publicGetter.publicGetter.publicGetSetter', 'bar'),
array(new TestClass(array('foo' => array('baz' => new TestClass('bar')))), 'publicGetter[foo][baz].publicGetSetter', 'bar'),
);
}
public function testTicket5755()
{
$object = new Ticket5775Object();
$this->propertyAccessor->setValue($object, 'property', 'foobar');
$this->assertEquals('foobar', $object->getProperty());
}
public function testSetValueDeepWithMagicGetter()
{
$obj = new TestClassMagicGet('foo');
$obj->publicProperty = array('foo' => array('bar' => 'some_value'));
$this->propertyAccessor->setValue($obj, 'publicProperty[foo][bar]', 'Updated');
$this->assertSame('Updated', $obj->publicProperty['foo']['bar']);
}
public function getReferenceChainObjectsForSetValue()
{
return array(
array(array('a' => array('b' => array('c' => 'old-value'))), '[a][b][c]', 'new-value'),
array(new TestClassSetValue(new TestClassSetValue('old-value')), 'value.value', 'new-value'),
array(new TestClassSetValue(array('a' => array('b' => array('c' => new TestClassSetValue('old-value'))))), 'value[a][b][c].value', 'new-value'),
array(new TestClassSetValue(array('a' => array('b' => 'old-value'))), 'value[a][b]', 'new-value'),
array(new \ArrayIterator(array('a' => array('b' => array('c' => 'old-value')))), '[a][b][c]', 'new-value'),
);
}
/**
* @dataProvider getReferenceChainObjectsForSetValue
*/
public function testSetValueForReferenceChainIssue($object, $path, $value)
{
$this->propertyAccessor->setValue($object, $path, $value);
$this->assertEquals($value, $this->propertyAccessor->getValue($object, $path));
}
public function getReferenceChainObjectsForIsWritable()
{
return array(
array(new TestClassIsWritable(array('a' => array('b' => 'old-value'))), 'value[a][b]', false),
array(new TestClassIsWritable(new \ArrayIterator(array('a' => array('b' => 'old-value')))), 'value[a][b]', true),
array(new TestClassIsWritable(array('a' => array('b' => array('c' => new TestClassSetValue('old-value'))))), 'value[a][b][c].value', true),
);
}
/**
* @dataProvider getReferenceChainObjectsForIsWritable
*/
public function testIsWritableForReferenceChainIssue($object, $path, $value)
{
$this->assertEquals($value, $this->propertyAccessor->isWritable($object, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
* @expectedExceptionMessage Expected argument of type "DateTime", "string" given
*/
public function testThrowTypeError()
{
$object = new TypeHinted();
$this->propertyAccessor->setValue($object, 'date', 'This is a string, \DateTime expected.');
}
public function testSetTypeHint()
{
$date = new \DateTime();
$object = new TypeHinted();
$this->propertyAccessor->setValue($object, 'date', $date);
$this->assertSame($date, $object->getDate());
}
public function testArrayNotBeeingOverwritten()
{
$value = array('value1' => 'foo', 'value2' => 'bar');
$object = new TestClass($value);
$this->propertyAccessor->setValue($object, 'publicAccessor[value2]', 'baz');
$this->assertSame('baz', $this->propertyAccessor->getValue($object, 'publicAccessor[value2]'));
$this->assertSame(array('value1' => 'foo', 'value2' => 'baz'), $object->getPublicAccessor());
}
public function testCacheReadAccess()
{
$obj = new TestClass('foo');
$propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter());
$this->assertEquals('foo', $propertyAccessor->getValue($obj, 'publicGetSetter'));
$propertyAccessor->setValue($obj, 'publicGetSetter', 'bar');
$propertyAccessor->setValue($obj, 'publicGetSetter', 'baz');
$this->assertEquals('baz', $propertyAccessor->getValue($obj, 'publicGetSetter'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
* @expectedExceptionMessage Expected argument of type "Countable", "string" given
*/
public function testThrowTypeErrorWithInterface()
{
$object = new TypeHinted();
$this->propertyAccessor->setValue($object, 'countable', 'This is a string, \Countable expected.');
}
public function testAnonymousClassRead()
{
$value = 'bar';
$obj = $this->generateAnonymousClass($value);
$propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter());
$this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo'));
}
public function testAnonymousClassWrite()
{
$value = 'bar';
$obj = $this->generateAnonymousClass('');
$propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter());
$propertyAccessor->setValue($obj, 'foo', $value);
$this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo'));
}
private function generateAnonymousClass($value)
{
$obj = eval('return new class($value)
{
private $foo;
public function __construct($foo)
{
$this->foo = $foo;
}
/**
* @return mixed
*/
public function getFoo()
{
return $this->foo;
}
/**
* @param mixed $foo
*/
public function setFoo($foo)
{
$this->foo = $foo;
}
};');
return $obj;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TraversableArrayObject;
class PropertyAccessorTraversableArrayObjectTest extends PropertyAccessorCollectionTest
{
protected function getContainer(array $array)
{
return new TraversableArrayObject($array);
}
}

View File

@@ -0,0 +1,288 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathBuilder;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathBuilderTest extends TestCase
{
const PREFIX = 'old1[old2].old3[old4][old5].old6';
/**
* @var PropertyPathBuilder
*/
private $builder;
protected function setUp()
{
$this->builder = new PropertyPathBuilder(new PropertyPath(self::PREFIX));
}
public function testCreateEmpty()
{
$builder = new PropertyPathBuilder();
$this->assertNull($builder->getPropertyPath());
}
public function testCreateCopyPath()
{
$this->assertEquals(new PropertyPath(self::PREFIX), $this->builder->getPropertyPath());
}
public function testAppendIndex()
{
$this->builder->appendIndex('new1');
$path = new PropertyPath(self::PREFIX.'[new1]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendProperty()
{
$this->builder->appendProperty('new1');
$path = new PropertyPath(self::PREFIX.'.new1');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppend()
{
$this->builder->append(new PropertyPath('new1[new2]'));
$path = new PropertyPath(self::PREFIX.'.new1[new2]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendUsingString()
{
$this->builder->append('new1[new2]');
$path = new PropertyPath(self::PREFIX.'.new1[new2]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendWithOffset()
{
$this->builder->append(new PropertyPath('new1[new2].new3'), 1);
$path = new PropertyPath(self::PREFIX.'[new2].new3');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendWithOffsetAndLength()
{
$this->builder->append(new PropertyPath('new1[new2].new3'), 1, 1);
$path = new PropertyPath(self::PREFIX.'[new2]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceByIndex()
{
$this->builder->replaceByIndex(1, 'new1');
$path = new PropertyPath('old1[new1].old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceByIndexWithoutName()
{
$this->builder->replaceByIndex(0);
$path = new PropertyPath('[old1][old2].old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByIndexDoesNotAllowInvalidOffsets()
{
$this->builder->replaceByIndex(6, 'new1');
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByIndexDoesNotAllowNegativeOffsets()
{
$this->builder->replaceByIndex(-1, 'new1');
}
public function testReplaceByProperty()
{
$this->builder->replaceByProperty(1, 'new1');
$path = new PropertyPath('old1.new1.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceByPropertyWithoutName()
{
$this->builder->replaceByProperty(1);
$path = new PropertyPath('old1.old2.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByPropertyDoesNotAllowInvalidOffsets()
{
$this->builder->replaceByProperty(6, 'new1');
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByPropertyDoesNotAllowNegativeOffsets()
{
$this->builder->replaceByProperty(-1, 'new1');
}
public function testReplace()
{
$this->builder->replace(1, 1, new PropertyPath('new1[new2].new3'));
$path = new PropertyPath('old1.new1[new2].new3.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceUsingString()
{
$this->builder->replace(1, 1, 'new1[new2].new3');
$path = new PropertyPath('old1.new1[new2].new3.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceNegative()
{
$this->builder->replace(-1, 1, new PropertyPath('new1[new2].new3'));
$path = new PropertyPath('old1[old2].old3[old4][old5].new1[new2].new3');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @dataProvider provideInvalidOffsets
* @expectedException \OutOfBoundsException
*/
public function testReplaceDoesNotAllowInvalidOffsets($offset)
{
$this->builder->replace($offset, 1, new PropertyPath('new1[new2].new3'));
}
public function provideInvalidOffsets()
{
return array(
array(6),
array(-7),
);
}
public function testReplaceWithLengthGreaterOne()
{
$this->builder->replace(0, 2, new PropertyPath('new1[new2].new3'));
$path = new PropertyPath('new1[new2].new3.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceSubstring()
{
$this->builder->replace(1, 1, new PropertyPath('new1[new2].new3.new4[new5]'), 1, 3);
$path = new PropertyPath('old1[new2].new3.new4.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceSubstringWithLengthGreaterOne()
{
$this->builder->replace(1, 2, new PropertyPath('new1[new2].new3.new4[new5]'), 1, 3);
$path = new PropertyPath('old1[new2].new3.new4[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
// https://github.com/symfony/symfony/issues/5605
public function testReplaceWithLongerPath()
{
// error occurs when path contains at least two more elements
// than the builder
$path = new PropertyPath('new1.new2.new3');
$builder = new PropertyPathBuilder(new PropertyPath('old1'));
$builder->replace(0, 1, $path);
$this->assertEquals($path, $builder->getPropertyPath());
}
public function testReplaceWithLongerPathKeepsOrder()
{
$path = new PropertyPath('new1.new2.new3');
$expected = new PropertyPath('new1.new2.new3.old2');
$builder = new PropertyPathBuilder(new PropertyPath('old1.old2'));
$builder->replace(0, 1, $path);
$this->assertEquals($expected, $builder->getPropertyPath());
}
public function testRemove()
{
$this->builder->remove(3);
$path = new PropertyPath('old1[old2].old3[old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @expectedException \OutOfBoundsException
*/
public function testRemoveDoesNotAllowInvalidOffsets()
{
$this->builder->remove(6);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testRemoveDoesNotAllowNegativeOffsets()
{
$this->builder->remove(-1);
}
}

View File

@@ -0,0 +1,206 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyAccess\PropertyPath;
class PropertyPathTest extends TestCase
{
public function testToString()
{
$path = new PropertyPath('reference.traversable[index].property');
$this->assertEquals('reference.traversable[index].property', $path->__toString());
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testDotIsRequiredBeforeProperty()
{
new PropertyPath('[index]property');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testDotCannotBePresentAtTheBeginning()
{
new PropertyPath('.property');
}
public function providePathsContainingUnexpectedCharacters()
{
return array(
array('property.'),
array('property.['),
array('property..'),
array('property['),
array('property[['),
array('property[.'),
array('property[]'),
);
}
/**
* @dataProvider providePathsContainingUnexpectedCharacters
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testUnexpectedCharacters($path)
{
new PropertyPath($path);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testPathCannotBeEmpty()
{
new PropertyPath('');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
*/
public function testPathCannotBeNull()
{
new PropertyPath(null);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
*/
public function testPathCannotBeFalse()
{
new PropertyPath(false);
}
public function testZeroIsValidPropertyPath()
{
$propertyPath = new PropertyPath('0');
$this->assertSame('0', (string) $propertyPath);
}
public function testGetParentWithDot()
{
$propertyPath = new PropertyPath('grandpa.parent.child');
$this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent());
}
public function testGetParentWithIndex()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent());
}
public function testGetParentWhenThereIsNoParent()
{
$propertyPath = new PropertyPath('path');
$this->assertNull($propertyPath->getParent());
}
public function testCopyConstructor()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$copy = new PropertyPath($propertyPath);
$this->assertEquals($propertyPath, $copy);
}
public function testGetElement()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertEquals('child', $propertyPath->getElement(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testGetElementDoesNotAcceptInvalidIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->getElement(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testGetElementDoesNotAcceptNegativeIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->getElement(-1);
}
public function testIsProperty()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertTrue($propertyPath->isProperty(1));
$this->assertFalse($propertyPath->isProperty(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsPropertyDoesNotAcceptInvalidIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isProperty(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsPropertyDoesNotAcceptNegativeIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isProperty(-1);
}
public function testIsIndex()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertFalse($propertyPath->isIndex(1));
$this->assertTrue($propertyPath->isIndex(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsIndexDoesNotAcceptInvalidIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isIndex(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsIndexDoesNotAcceptNegativeIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isIndex(-1);
}
}

View File

@@ -0,0 +1,40 @@
{
"name": "symfony/property-access",
"type": "library",
"description": "Symfony PropertyAccess Component",
"keywords": ["property", "index", "access", "object", "array", "extraction", "injection", "reflection", "property path"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": "^7.1.3",
"symfony/inflector": "~3.4|~4.0"
},
"require-dev": {
"symfony/cache": "~3.4|~4.0"
},
"suggest": {
"psr/cache-implementation": "To cache access methods."
},
"autoload": {
"psr-4": { "Symfony\\Component\\PropertyAccess\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
}
}
}

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony PropertyAccess Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>