[PropertyAccess] Extracted PropertyAccess component out of Form

This commit is contained in:
Bernhard Schussek 2013-01-07 09:19:31 +01:00
parent b981a6fa60
commit 1bae7b242c
73 changed files with 2706 additions and 1890 deletions

View File

@ -84,6 +84,46 @@
{{ error.message }}
```
* FormType, ModelType and PropertyPathMapper now have constructors. If you
extended these classes, you should call the parent constructor now.
Note that you are not recommended to extend FormType nor ModelType. You should
extend AbstractType instead and use the Form component's own inheritance
mechanism (`AbstractType::getParent()`).
Before:
```
use Symfony\Component\Form\Extensions\Core\DataMapper\PropertyPathMapper;
class CustomMapper extends PropertyPathMapper
{
public function __construct()
{
// ...
}
// ...
}
```
After:
```
use Symfony\Component\Form\Extensions\Core\DataMapper\PropertyPathMapper;
class CustomMapper extends PropertyPathMapper
{
public function __construct()
{
parent::__construct();
// ...
}
// ...
}
```
#### Deprecations
* The methods `getParent()`, `setParent()` and `hasParent()` in
@ -91,6 +131,94 @@
You should not rely on these methods in your form type because the parent
of a form can change after building it.
* The class PropertyPath and related classes were deprecated and moved to a
dedicated component PropertyAccess. If you used any of these classes or
interfaces, you should adapt the namespaces now. During the move,
InvalidPropertyException was renamed to NoSuchPropertyException.
Before:
```
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Util\PropertyPathBuilder;
use Symfony\Component\Form\Util\PropertyPathInterface;
use Symfony\Component\Form\Util\PropertyPathIterator;
use Symfony\Component\Form\Util\PropertyPathIteratorInterface;
use Symfony\Component\Form\Exception\InvalidPropertyException;
use Symfony\Component\Form\Exception\InvalidPropertyPathException;
use Symfony\Component\Form\Exception\PropertyAccessDeniedException;
```
After:
```
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathBuilder;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
use Symfony\Component\PropertyAccess\PropertyPathIterator;
use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
use Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException;
```
Also, `FormUtil::singularify()` was split away into a class StringUtil
in the new component.
Before:
```
use Symfony\Component\Form\Util\FormUtil;
$singular = FormUtil::singularify($plural);
```
After:
```
use Symfony\Component\PropertyAccess\StringUtil;
$singular = StringUtil::singularify($plural);
```
The methods `getValue()` and `setValue()` were moved to a new class
PropertyAccessor.
Before:
```
use Symfony\Component\Form\Util\PropertyPath;
$propertyPath = new PropertyPath('some.path');
$value = $propertyPath->getValue($object);
$propertyPath->setValue($object, 'new value');
```
After (alternative 1):
```
use Symfony\Component\PropertyAccess\PropertyAccess;
$accessor = PropertyAccess::getPropertyAccessor();
$value = $propertyAccessor->getValue($object, 'some.path');
$accessor->setValue($object, 'some.path', 'new value');
```
After (alternative 2):
```
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyPath;
$accessor = PropertyAccess::getPropertyAccessor();
$propertyPath = new PropertyPath('some.path');
$value = $propertyAccessor->getValue($object, $propertyPath);
$accessor->setValue($object, $propertyPath, 'new value');
```
### Routing
* RouteCollection does not behave like a tree structure anymore but as a flat

View File

@ -1,6 +1,12 @@
CHANGELOG
=========
2.2.0
-----
* added an optional PropertyAccessorInterface parameter to DoctrineType,
EntityType and EntityChoiceList
2.1.0
-----

View File

@ -15,6 +15,7 @@ use Symfony\Component\Form\Exception\Exception;
use Symfony\Component\Form\Exception\StringCastException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* A choice list presenting a list of Doctrine entities as choices
@ -86,17 +87,18 @@ class EntityChoiceList extends ObjectChoiceList
/**
* Creates a new entity choice list.
*
* @param ObjectManager $manager An EntityManager instance
* @param string $class The class name
* @param string $labelPath The property path used for the label
* @param EntityLoaderInterface $entityLoader An optional query builder
* @param array $entities An array of choices
* @param array $preferredEntities An array of preferred choices
* @param string $groupPath A property path pointing to the property used
* to group the choices. Only allowed if
* the choices are given as flat array.
* @param ObjectManager $manager An EntityManager instance
* @param string $class The class name
* @param string $labelPath The property path used for the label
* @param EntityLoaderInterface $entityLoader An optional query builder
* @param array $entities An array of choices
* @param array $preferredEntities An array of preferred choices
* @param string $groupPath A property path pointing to the property used
* to group the choices. Only allowed if
* the choices are given as flat array.
* @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths.
*/
public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null)
public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null, PropertyAccessorInterface $propertyAccessor = null)
{
$this->em = $manager;
$this->entityLoader = $entityLoader;
@ -122,7 +124,7 @@ class EntityChoiceList extends ObjectChoiceList
$entities = array();
}
parent::__construct($entities, $labelPath, $preferredEntities, $groupPath);
parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, null, $propertyAccessor);
}
/**

View File

@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Form;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\PropertyAccess\PropertyAccess;
class DoctrineOrmExtension extends AbstractExtension
{
@ -26,7 +27,7 @@ class DoctrineOrmExtension extends AbstractExtension
protected function loadTypes()
{
return array(
new Type\EntityType($this->registry),
new Type\EntityType($this->registry, PropertyAccess::getPropertyAccessor()),
);
}

View File

@ -22,6 +22,8 @@ use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
abstract class DoctrineType extends AbstractType
{
@ -35,9 +37,15 @@ abstract class DoctrineType extends AbstractType
*/
private $choiceListCache = array();
public function __construct(ManagerRegistry $registry)
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null)
{
$this->registry = $registry;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor();
}
public function buildForm(FormBuilderInterface $builder, array $options)
@ -54,6 +62,7 @@ abstract class DoctrineType extends AbstractType
{
$choiceListCache =& $this->choiceListCache;
$registry = $this->registry;
$propertyAccessor = $this->propertyAccessor;
$type = $this;
$loader = function (Options $options) use ($type) {
@ -64,7 +73,7 @@ abstract class DoctrineType extends AbstractType
return null;
};
$choiceList = function (Options $options) use (&$choiceListCache, &$time) {
$choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) {
// Support for closures
$propertyHash = is_object($options['property'])
? spl_object_hash($options['property'])
@ -118,7 +127,8 @@ abstract class DoctrineType extends AbstractType
$options['loader'],
$options['choices'],
$options['preferred_choices'],
$options['group_by']
$options['group_by'],
$propertyAccessor
);
}

View File

@ -90,6 +90,7 @@ class EntityTypeTest extends TypeTestCase
parent::tearDown();
$this->em = null;
$this->emRegistry = null;
}
protected function getExtensions()

View File

@ -5,6 +5,9 @@ CHANGELOG
-----
* added a collection type for the I18n behavior
* added an optional PropertyAccessorInterface parameter to ModelType and
ModelChoiceList
* [BC BREAK] ModelType now has a constructor
2.1.0
-----

View File

@ -18,6 +18,7 @@ use \Persistent;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\StringCastException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* Widely inspired by the EntityChoiceList.
@ -69,16 +70,17 @@ class ModelChoiceList extends ObjectChoiceList
*
* @see Symfony\Bridge\Propel1\Form\Type\ModelType How to use the preferred choices.
*
* @param string $class The FQCN of the model class to be loaded.
* @param string $labelPath A property path pointing to the property used for the choice labels.
* @param array $choices An optional array to use, rather than fetching the models.
* @param ModelCriteria $queryObject The query to use retrieving model data from database.
* @param string $groupPath A property path pointing to the property used to group the choices.
* @param array|ModelCriteria $preferred The preferred items of this choice.
* Either an array if $choices is given,
* or a ModelCriteria to be merged with the $queryObject.
* @param string $class The FQCN of the model class to be loaded.
* @param string $labelPath A property path pointing to the property used for the choice labels.
* @param array $choices An optional array to use, rather than fetching the models.
* @param ModelCriteria $queryObject The query to use retrieving model data from database.
* @param string $groupPath A property path pointing to the property used to group the choices.
* @param array|ModelCriteria $preferred The preferred items of this choice.
* Either an array if $choices is given,
* or a ModelCriteria to be merged with the $queryObject.
* @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths.
*/
public function __construct($class, $labelPath = null, $choices = null, $queryObject = null, $groupPath = null, $preferred = array())
public function __construct($class, $labelPath = null, $choices = null, $queryObject = null, $groupPath = null, $preferred = array(), PropertyAccessorInterface $propertyAccessor = null)
{
$this->class = $class;
@ -104,7 +106,7 @@ class ModelChoiceList extends ObjectChoiceList
$this->identifierAsIndex = true;
}
parent::__construct($choices, $labelPath, $preferred, $groupPath);
parent::__construct($choices, $labelPath, $preferred, $groupPath, null, $propertyAccessor);
}
/**

View File

@ -12,6 +12,7 @@
namespace Symfony\Bridge\Propel1\Form;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\PropertyAccess\PropertyAccess;
/**
* Represents the Propel form extension, which loads the Propel functionality.
@ -23,7 +24,7 @@ class PropelExtension extends AbstractExtension
protected function loadTypes()
{
return array(
new Type\ModelType(),
new Type\ModelType(PropertyAccess::getPropertyAccessor()),
new Type\TranslationCollectionType(),
new Type\TranslationType()
);

View File

@ -17,6 +17,8 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* ModelType class.
@ -48,6 +50,16 @@ use Symfony\Component\OptionsResolver\OptionsResolverInterface;
*/
class ModelType extends AbstractType
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor();
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['multiple']) {
@ -57,14 +69,17 @@ class ModelType extends AbstractType
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choiceList = function (Options $options) {
$propertyAccessor = $this->propertyAccessor;
$choiceList = function (Options $options) use ($propertyAccessor) {
return new ModelChoiceList(
$options['class'],
$options['property'],
$options['choices'],
$options['query'],
$options['group_by'],
$options['preferred_choices']
$options['preferred_choices'],
$propertyAccessor
);
};

View File

@ -26,6 +26,10 @@ class ModelChoiceListTest extends Propel1TestCase
if (!class_exists('Symfony\Component\Form\Form')) {
$this->markTestSkipped('The "Form" component is not available');
}
if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccessor')) {
$this->markTestSkipped('The "PropertyAccessor" component is not available');
}
}
public function testEmptyChoicesReturnsEmpty()

View File

@ -10,6 +10,7 @@
<parameter key="form.factory.class">Symfony\Component\Form\FormFactory</parameter>
<parameter key="form.extension.class">Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension</parameter>
<parameter key="form.type_guesser.validator.class">Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser</parameter>
<parameter key="property_accessor.class">Symfony\Component\PropertyAccess\PropertyAccessor</parameter>
</parameters>
<services>
@ -53,11 +54,15 @@
<argument type="service" id="validator.mapping.class_metadata_factory" />
</service>
<!-- PropertyAccessor -->
<service id="property_accessor" class="%property_accessor.class%" />
<!-- CoreExtension -->
<service id="form.type.field" class="Symfony\Component\Form\Extension\Core\Type\FieldType">
<tag name="form.type" alias="field" />
</service>
<service id="form.type.form" class="Symfony\Component\Form\Extension\Core\Type\FormType">
<argument type="service" id="property_accessor"/>
<tag name="form.type" alias="form" />
</service>
<service id="form.type.birthday" class="Symfony\Component\Form\Extension\Core\Type\BirthdayType">

View File

@ -13,6 +13,15 @@ CHANGELOG
* [BC BREAK] FormException is now an interface
* protected FormBuilder methods from being called when it is turned into a FormConfigInterface with getFormConfig()
* [BC BREAK] inserted argument `$message` in the constructor of `FormError`
* the PropertyPath class and related classes were moved to a dedicated
PropertyAccess component. During the move, InvalidPropertyException was
renamed to NoSuchPropertyException. FormUtil was split: FormUtil::singularify()
can now be found in Symfony\Component\PropertyAccess\StringUtil. The methods
getValue() and setValue() from PropertyPath were extracted into a new class
PropertyAccessor.
* added an optional PropertyAccessorInterface parameter to FormType,
ObjectChoiceList and PropertyPathMapper
* [BC BREAK] PropertyPathMapper and FormType now have a constructor
2.1.0
-----

View File

@ -11,9 +11,11 @@
namespace Symfony\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Exception\StringCastException;
use Symfony\Component\Form\Exception\InvalidPropertyException;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* A choice list for object choices.
@ -32,6 +34,11 @@ use Symfony\Component\Form\Exception\InvalidPropertyException;
*/
class ObjectChoiceList extends ChoiceList
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* The property path used to obtain the choice label.
*
@ -56,28 +63,30 @@ class ObjectChoiceList extends ChoiceList
/**
* Creates a new object choice list.
*
* @param array|\Traversable $choices The array of choices. Choices may also be given
* as hierarchy of unlimited depth by creating nested
* arrays. The title of the sub-hierarchy can be
* stored in the array key pointing to the nested
* array. The topmost level of the hierarchy may also
* be a \Traversable.
* @param string $labelPath A property path pointing to the property used
* for the choice labels. The value is obtained
* by calling the getter on the object. If the
* path is NULL, the object's __toString() method
* is used instead.
* @param array $preferredChoices A flat array of choices that should be
* presented to the user with priority.
* @param string $groupPath A property path pointing to the property used
* to group the choices. Only allowed if
* the choices are given as flat array.
* @param string $valuePath A property path pointing to the property used
* for the choice values. If not given, integers
* are generated instead.
* @param array|\Traversable $choices The array of choices. Choices may also be given
* as hierarchy of unlimited depth by creating nested
* arrays. The title of the sub-hierarchy can be
* stored in the array key pointing to the nested
* array. The topmost level of the hierarchy may also
* be a \Traversable.
* @param string $labelPath A property path pointing to the property used
* for the choice labels. The value is obtained
* by calling the getter on the object. If the
* path is NULL, the object's __toString() method
* is used instead.
* @param array $preferredChoices A flat array of choices that should be
* presented to the user with priority.
* @param string $groupPath A property path pointing to the property used
* to group the choices. Only allowed if
* the choices are given as flat array.
* @param string $valuePath A property path pointing to the property used
* for the choice values. If not given, integers
* are generated instead.
* @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths.
*/
public function __construct($choices, $labelPath = null, array $preferredChoices = array(), $groupPath = null, $valuePath = null)
public function __construct($choices, $labelPath = null, array $preferredChoices = array(), $groupPath = null, $valuePath = null, PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor();
$this->labelPath = null !== $labelPath ? new PropertyPath($labelPath) : null;
$this->groupPath = null !== $groupPath ? new PropertyPath($groupPath) : null;
$this->valuePath = null !== $valuePath ? new PropertyPath($valuePath) : null;
@ -108,8 +117,8 @@ class ObjectChoiceList extends ChoiceList
}
try {
$group = $this->groupPath->getValue($choice);
} catch (InvalidPropertyException $e) {
$group = $this->propertyAccessor->getValue($choice, $this->groupPath);
} catch (NoSuchPropertyException $e) {
// Don't group items whose group property does not exist
// see https://github.com/symfony/symfony/commit/d9b7abb7c7a0f28e0ce970afc5e305dce5dccddf
$group = null;
@ -150,7 +159,7 @@ class ObjectChoiceList extends ChoiceList
protected function createValue($choice)
{
if ($this->valuePath) {
return (string) $this->valuePath->getValue($choice);
return (string) $this->propertyAccessor->getValue($choice, $this->valuePath);
}
return parent::createValue($choice);
@ -163,7 +172,7 @@ class ObjectChoiceList extends ChoiceList
$labels[$i] = array();
$this->extractLabels($choice, $labels[$i]);
} elseif ($this->labelPath) {
$labels[$i] = $this->labelPath->getValue($choice);
$labels[$i] = $this->propertyAccessor->getValue($choice, $this->labelPath);
} elseif (method_exists($choice, '__toString')) {
$labels[$i] = (string) $choice;
} else {

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Core;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\PropertyAccess\PropertyAccess;
/**
* Represents the main form extension, which loads the core functionality.
@ -24,7 +25,7 @@ class CoreExtension extends AbstractExtension
{
return array(
new Type\FieldType(),
new Type\FormType(),
new Type\FormType(PropertyAccess::getPropertyAccessor()),
new Type\BirthdayType(),
new Type\CheckboxType(),
new Type\ChoiceType(),

View File

@ -14,9 +14,31 @@ namespace Symfony\Component\Form\Extension\Core\DataMapper;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Util\VirtualFormAwareIterator;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* A data mapper using property paths to read/write data.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathMapper implements DataMapperInterface
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* Creates a new property path mapper.
*
* @param PropertyAccessorInterface $propertyAccessor
*/
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor();
}
/**
* {@inheritdoc}
*/
@ -39,7 +61,7 @@ class PropertyPathMapper implements DataMapperInterface
$config = $form->getConfig();
if (null !== $propertyPath && $config->getMapped()) {
$form->setData($propertyPath->getValue($data));
$form->setData($this->propertyAccessor->getValue($data, $propertyPath));
}
}
}
@ -70,8 +92,8 @@ class PropertyPathMapper implements DataMapperInterface
if (null !== $propertyPath && $config->getMapped() && $form->isSynchronized() && !$form->isDisabled()) {
// If the data is identical to the value in $data, we are
// dealing with a reference
if (!is_object($data) || !$config->getByReference() || $form->getData() !== $propertyPath->getValue($data)) {
$propertyPath->setValue($data, $form->getData());
if (!is_object($data) || !$config->getByReference() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) {
$this->propertyAccessor->setValue($data, $propertyPath, $form->getData());
}
}
}

View File

@ -20,9 +20,21 @@ use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Exception\Exception;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class FormType extends AbstractType
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor();
}
/**
* {@inheritdoc}
*/
@ -41,7 +53,7 @@ class FormType extends AbstractType
->setCompound($options['compound'])
->setData(isset($options['data']) ? $options['data'] : null)
->setDataLocked(isset($options['data']))
->setDataMapper($options['compound'] ? new PropertyPathMapper() : null)
->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null)
;
if ($options['trim']) {

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPath;
/**
* @author Bernhard Schussek <bschussek@gmail.com>

View File

@ -13,9 +13,9 @@ namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Util\VirtualFormAwareIterator;
use Symfony\Component\Form\Util\PropertyPathIterator;
use Symfony\Component\Form\Util\PropertyPathBuilder;
use Symfony\Component\Form\Util\PropertyPathIteratorInterface;
use Symfony\Component\PropertyAccess\PropertyPathIterator;
use Symfony\Component\PropertyAccess\PropertyPathBuilder;
use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationPathIterator;
use Symfony\Component\Form\FormError;
use Symfony\Component\Validator\ConstraintViolation;
@ -265,7 +265,7 @@ class ViolationMapper implements ViolationMapperInterface
$propertyPathBuilder->remove(0, $i + 1);
$i = 0;
} else {
/* @var \Symfony\Component\Form\Util\PropertyPathInterface $propertyPath */
/* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */
$propertyPath = $scope->getPropertyPath();
if (null === $propertyPath) {

View File

@ -11,8 +11,8 @@
namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Util\PropertyPathInterface;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
use Symfony\Component\Form\Util\PropertyPathIterator;
use Symfony\Component\PropertyAccess\PropertyPathIterator;
/**
* @author Bernhard Schussek <bschussek@gmail.com>

View File

@ -16,8 +16,8 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\AlreadyBoundException;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PropertyAccess\PropertyPath;
/**
* Form represents a form.

View File

@ -14,8 +14,8 @@ namespace Symfony\Component\Form;
use Symfony\Component\Form\Exception\BadMethodCallException;
use Symfony\Component\Form\Exception\Exception;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Util\PropertyPathInterface;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;

View File

@ -117,7 +117,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface
/**
* Sets the data mapper used by the form.
*
* @param DataMapperInterface $dataMapper
* @param DataMapperInterface $dataMapper
*
* @return self The configuration object.
*/
@ -126,7 +126,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface
/**
* Set whether the form is disabled.
*
* @param Boolean $disabled Whether the form is disabled
* @param Boolean $disabled Whether the form is disabled
*
* @return self The configuration object.
*/
@ -135,7 +135,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface
/**
* Sets the data used for the client data when no value is bound.
*
* @param mixed $emptyData The empty data.
* @param mixed $emptyData The empty data.
*
* @return self The configuration object.
*/
@ -144,7 +144,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface
/**
* Sets whether errors bubble up to the parent.
*
* @param Boolean $errorBubbling
* @param Boolean $errorBubbling
*
* @return self The configuration object.
*/
@ -162,9 +162,9 @@ interface FormConfigBuilderInterface extends FormConfigInterface
/**
* Sets the property path that the form should be mapped to.
*
* @param null|string|PropertyPathInterface $propertyPath The property path or null if the path
* should be set automatically based on
* the form's name.
* @param null|string|\Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath
* The property path or null if the path should be set
* automatically based on the form's name.
*
* @return self The configuration object.
*/
@ -174,7 +174,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface
* Sets whether the form should be mapped to an element of its
* parent's data.
*
* @param Boolean $mapped Whether the form should be mapped.
* @param Boolean $mapped Whether the form should be mapped.
*
* @return self The configuration object.
*/
@ -183,7 +183,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface
/**
* Sets whether the form's data should be modified by reference.
*
* @param Boolean $byReference Whether the data should be
* @param Boolean $byReference Whether the data should be
* modified by reference.
*
* @return self The configuration object.
@ -193,7 +193,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface
/**
* Sets whether the form should be virtual.
*
* @param Boolean $virtual Whether the form should be virtual.
* @param Boolean $virtual Whether the form should be virtual.
*
* @return self The configuration object.
*/
@ -202,7 +202,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface
/**
* Sets whether the form should be compound.
*
* @param Boolean $compound Whether the form should be compound.
* @param Boolean $compound Whether the form should be compound.
*
* @return self The configuration object.
*
@ -235,7 +235,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface
* this configuration. The data can only be modified then by
* binding the form.
*
* @param Boolean $locked Whether to lock the default data.
* @param Boolean $locked Whether to lock the default data.
*
* @return self The configuration object.
*/

View File

@ -35,7 +35,7 @@ interface FormConfigInterface
/**
* Returns the property path that the form should be mapped to.
*
* @return null|Util\PropertyPathInterface The property path.
* @return null|\Symfony\Component\PropertyAccess\PropertyPathInterface The property path.
*/
public function getPropertyPath();

View File

@ -166,7 +166,7 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
/**
* Returns the property path that the form is mapped to.
*
* @return Util\PropertyPathInterface The property path.
* @return \Symfony\Component\PropertyAccess\PropertyPathInterface The property path.
*/
public function getPropertyPath();

View File

@ -39,6 +39,9 @@ class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase
private $obj4;
/**
* @var ObjectChoiceList
*/
private $list;
protected function setUp()

View File

@ -11,10 +11,8 @@
namespace Symfony\Component\Form\Tests\Extension\Core\DataMapper;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormConfigBuilder;
use Symfony\Component\Form\FormConfigInterface;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
@ -29,14 +27,24 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
*/
private $dispatcher;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $propertyAccessor;
protected function setUp()
{
if (!class_exists('Symfony\Component\EventDispatcher\Event')) {
$this->markTestSkipped('The "EventDispatcher" component is not available');
}
if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccess')) {
$this->markTestSkipped('The "PropertyAccess" component is not available');
}
$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->mapper = new PropertyPathMapper();
$this->propertyAccessor = $this->getMock('Symfony\Component\PropertyAccess\PropertyAccessorInterface');
$this->mapper = new PropertyPathMapper($this->propertyAccessor);
}
/**
@ -45,7 +53,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
*/
private function getPropertyPath($path)
{
return $this->getMockBuilder('Symfony\Component\Form\Util\PropertyPath')
return $this->getMockBuilder('Symfony\Component\PropertyAccess\PropertyPath')
->setConstructorArgs(array($path))
->setMethods(array('getValue', 'setValue'))
->getMock();
@ -84,9 +92,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->once())
$this->propertyAccessor->expects($this->once())
->method('getValue')
->with($car)
->with($car, $propertyPath)
->will($this->returnValue($engine));
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
@ -107,9 +115,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->once())
$this->propertyAccessor->expects($this->once())
->method('getValue')
->with($car)
->with($car, $propertyPath)
->will($this->returnValue($engine));
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
@ -143,7 +151,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$car = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
$this->propertyAccessor->expects($this->never())
->method('getValue');
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
@ -161,7 +169,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
{
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
$this->propertyAccessor->expects($this->never())
->method('getValue');
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
@ -180,9 +188,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->once())
$this->propertyAccessor->expects($this->once())
->method('getValue')
->with($car)
->with($car, $propertyPath)
->will($this->returnValue($engine));
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
@ -211,9 +219,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->once())
$this->propertyAccessor->expects($this->once())
->method('setValue')
->with($car, $engine);
->with($car, $propertyPath, $engine);
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
$config->setByReference(false);
@ -230,9 +238,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->once())
$this->propertyAccessor->expects($this->once())
->method('setValue')
->with($car, $engine);
->with($car, $propertyPath, $engine);
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
$config->setByReference(true);
@ -250,12 +258,12 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$propertyPath = $this->getPropertyPath('engine');
// $car already contains the reference of $engine
$propertyPath->expects($this->once())
$this->propertyAccessor->expects($this->once())
->method('getValue')
->with($car)
->with($car, $propertyPath)
->will($this->returnValue($engine));
$propertyPath->expects($this->never())
$this->propertyAccessor->expects($this->never())
->method('setValue');
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
@ -273,7 +281,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
$this->propertyAccessor->expects($this->never())
->method('setValue');
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
@ -291,7 +299,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$car = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
$this->propertyAccessor->expects($this->never())
->method('setValue');
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
@ -309,7 +317,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
$this->propertyAccessor->expects($this->never())
->method('setValue');
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
@ -327,7 +335,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
$this->propertyAccessor->expects($this->never())
->method('setValue');
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
@ -347,14 +355,14 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$parentPath = $this->getPropertyPath('name');
$childPath = $this->getPropertyPath('engine');
$parentPath->expects($this->never())
->method('getValue');
$parentPath->expects($this->never())
->method('setValue');
// getValue() and setValue() must never be invoked for $parentPath
$childPath->expects($this->once())
$this->propertyAccessor->expects($this->once())
->method('getValue')
->with($car, $childPath);
$this->propertyAccessor->expects($this->once())
->method('setValue')
->with($car, $engine);
->with($car, $childPath, $engine);
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
$config->setPropertyPath($parentPath);

View File

@ -11,8 +11,6 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Tests\Fixtures\Author;

View File

@ -14,7 +14,7 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\EventListener;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
use Symfony\Component\Validator\ConstraintViolation;

View File

@ -17,7 +17,7 @@ use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormConfigBuilder;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Validator\ConstraintViolation;
/**

View File

@ -14,7 +14,7 @@ namespace Symfony\Component\Form\Tests;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Form\FormConfigBuilder;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Exception\TransformationFailedException;

View File

@ -1,557 +0,0 @@
<?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\Form\Tests\Util;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Tests\Fixtures\Author;
use Symfony\Component\Form\Tests\Fixtures\Magician;
class PropertyPathTest extends \PHPUnit_Framework_TestCase
{
public function testGetValueReadsArray()
{
$array = array('firstName' => 'Bernhard');
$path = new PropertyPath('[firstName]');
$this->assertEquals('Bernhard', $path->getValue($array));
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testGetValueThrowsExceptionIfIndexNotationExpected()
{
$array = array('firstName' => 'Bernhard');
$path = new PropertyPath('firstName');
$path->getValue($array);
}
public function testGetValueReadsZeroIndex()
{
$array = array('Bernhard');
$path = new PropertyPath('[0]');
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueReadsIndexWithSpecialChars()
{
$array = array('%!@$§.' => 'Bernhard');
$path = new PropertyPath('[%!@$§.]');
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueReadsNestedIndexWithSpecialChars()
{
$array = array('root' => array('%!@$§.' => 'Bernhard'));
$path = new PropertyPath('[root][%!@$§.]');
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueReadsArrayWithCustomPropertyPath()
{
$array = array('child' => array('index' => array('firstName' => 'Bernhard')));
$path = new PropertyPath('[child][index][firstName]');
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath()
{
$array = array('child' => array('index' => array()));
$path = new PropertyPath('[child][index][firstName]');
$this->assertNull($path->getValue($array));
}
public function testGetValueReadsProperty()
{
$object = new Author();
$object->firstName = 'Bernhard';
$path = new PropertyPath('firstName');
$this->assertEquals('Bernhard', $path->getValue($object));
}
public function testGetValueIgnoresSingular()
{
$this->markTestSkipped('This feature is temporarily disabled as of 2.1');
$object = (object) array('children' => 'Many');
$path = new PropertyPath('children|child');
$this->assertEquals('Many', $path->getValue($object));
}
public function testGetValueReadsPropertyWithSpecialCharsExceptDot()
{
$array = (object) array('%!@$§' => 'Bernhard');
$path = new PropertyPath('%!@$§');
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueReadsPropertyWithCustomPropertyPath()
{
$object = new Author();
$object->child = array();
$object->child['index'] = new Author();
$object->child['index']->firstName = 'Bernhard';
$path = new PropertyPath('child[index].firstName');
$this->assertEquals('Bernhard', $path->getValue($object));
}
/**
* @expectedException \Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfPropertyIsNotPublic()
{
$path = new PropertyPath('privateProperty');
$path->getValue(new Author());
}
public function testGetValueReadsGetters()
{
$path = new PropertyPath('lastName');
$object = new Author();
$object->setLastName('Schussek');
$this->assertEquals('Schussek', $path->getValue($object));
}
public function testGetValueCamelizesGetterNames()
{
$path = new PropertyPath('last_name');
$object = new Author();
$object->setLastName('Schussek');
$this->assertEquals('Schussek', $path->getValue($object));
}
/**
* @expectedException \Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfGetterIsNotPublic()
{
$path = new PropertyPath('privateGetter');
$path->getValue(new Author());
}
public function testGetValueReadsIssers()
{
$path = new PropertyPath('australian');
$object = new Author();
$object->setAustralian(false);
$this->assertFalse($path->getValue($object));
}
public function testGetValueReadHassers()
{
$path = new PropertyPath('read_permissions');
$object = new Author();
$object->setReadPermissions(true);
$this->assertTrue($path->getValue($object));
}
public function testGetValueReadsMagicGet()
{
$path = new PropertyPath('magicProperty');
$object = new Magician();
$object->__set('magicProperty', 'foobar');
$this->assertSame('foobar', $path->getValue($object));
}
/*
* https://github.com/symfony/symfony/pull/4450
*/
public function testGetValueReadsMagicGetThatReturnsConstant()
{
$path = new PropertyPath('magicProperty');
$object = new Magician();
$this->assertNull($path->getValue($object));
}
/**
* @expectedException \Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfIsserIsNotPublic()
{
$path = new PropertyPath('privateIsser');
$path->getValue(new Author());
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testGetValueThrowsExceptionIfPropertyDoesNotExist()
{
$path = new PropertyPath('foobar');
$path->getValue(new Author());
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfNotObjectOrArray()
{
$path = new PropertyPath('foobar');
$path->getValue('baz');
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfNull()
{
$path = new PropertyPath('foobar');
$path->getValue(null);
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfEmpty()
{
$path = new PropertyPath('foobar');
$path->getValue('');
}
public function testSetValueUpdatesArrays()
{
$array = array();
$path = new PropertyPath('[firstName]');
$path->setValue($array, 'Bernhard');
$this->assertEquals(array('firstName' => 'Bernhard'), $array);
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testSetValueThrowsExceptionIfIndexNotationExpected()
{
$array = array();
$path = new PropertyPath('firstName');
$path->setValue($array, 'Bernhard');
}
public function testSetValueUpdatesArraysWithCustomPropertyPath()
{
$array = array();
$path = new PropertyPath('[child][index][firstName]');
$path->setValue($array, 'Bernhard');
$this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array);
}
public function testSetValueUpdatesProperties()
{
$object = new Author();
$path = new PropertyPath('firstName');
$path->setValue($object, 'Bernhard');
$this->assertEquals('Bernhard', $object->firstName);
}
public function testSetValueUpdatesPropertiesWithCustomPropertyPath()
{
$object = new Author();
$object->child = array();
$object->child['index'] = new Author();
$path = new PropertyPath('child[index].firstName');
$path->setValue($object, 'Bernhard');
$this->assertEquals('Bernhard', $object->child['index']->firstName);
}
public function testSetValueUpdateMagicSet()
{
$object = new Magician();
$path = new PropertyPath('magicProperty');
$path->setValue($object, 'foobar');
$this->assertEquals('foobar', $object->__get('magicProperty'));
}
public function testSetValueUpdatesSetters()
{
$object = new Author();
$path = new PropertyPath('lastName');
$path->setValue($object, 'Schussek');
$this->assertEquals('Schussek', $object->getLastName());
}
public function testSetValueCamelizesSetterNames()
{
$object = new Author();
$path = new PropertyPath('last_name');
$path->setValue($object, 'Schussek');
$this->assertEquals('Schussek', $object->getLastName());
}
/**
* @expectedException \Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testSetValueThrowsExceptionIfGetterIsNotPublic()
{
$path = new PropertyPath('privateSetter');
$path->setValue(new Author(), 'foobar');
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfNotObjectOrArray()
{
$path = new PropertyPath('foobar');
$value = 'baz';
$path->setValue($value, 'bam');
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfNull()
{
$path = new PropertyPath('foobar');
$value = null;
$path->setValue($value, 'bam');
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfEmpty()
{
$path = new PropertyPath('foobar');
$value = '';
$path->setValue($value, 'bam');
}
public function testToString()
{
$path = new PropertyPath('reference.traversable[index].property');
$this->assertEquals('reference.traversable[index].property', $path->__toString());
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_noDotBeforeProperty()
{
new PropertyPath('[index]property');
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_dotAtTheBeginning()
{
new PropertyPath('.property');
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_unexpectedCharacters()
{
new PropertyPath('property.$form');
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_empty()
{
new PropertyPath('');
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testInvalidPropertyPath_null()
{
new PropertyPath(null);
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testInvalidPropertyPath_false()
{
new PropertyPath(false);
}
public function testValidPropertyPath_zero()
{
new PropertyPath('0');
}
public function testGetParent_dot()
{
$propertyPath = new PropertyPath('grandpa.parent.child');
$this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent());
}
public function testGetParent_index()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent());
}
public function testGetParent_noParent()
{
$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

@ -11,181 +11,29 @@
namespace Symfony\Component\Form\Util;
use Symfony\Component\PropertyAccess\StringUtil;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormUtil
{
/**
* Map english plural to singular suffixes
*
* @var array
*
* @see http://english-zone.com/spelling/plurals.html
* @see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
*/
private static $pluralMap = array(
// First entry: plural suffix, reversed
// Second entry: length of plural suffix
// Third entry: Whether the suffix may succeed a vocal
// Fourth entry: Whether the suffix may succeed a consonant
// Fifth entry: singular suffix, normal
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
array('a', 1, true, true, array('on', 'um')),
// nebulae (nebula)
array('ea', 2, true, true, 'a'),
// mice (mouse), lice (louse)
array('eci', 3, false, true, 'ouse'),
// geese (goose)
array('esee', 4, false, true, 'oose'),
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
array('i', 1, true, true, 'us'),
// men (man), women (woman)
array('nem', 3, true, true, 'man'),
// children (child)
array('nerdlihc', 8, true, true, 'child'),
// oxen (ox)
array('nexo', 4, false, false, 'ox'),
// indices (index), appendices (appendix), prices (price)
array('seci', 4, false, true, array('ex', 'ix', 'ice')),
// babies (baby)
array('sei', 3, false, true, 'y'),
// analyses (analysis), ellipses (ellipsis), funguses (fungus),
// neuroses (neurosis), theses (thesis), emphases (emphasis),
// oases (oasis), crises (crisis), houses (house), bases (base),
// atlases (atlas), kisses (kiss)
array('ses', 3, true, true, array('s', 'se', 'sis')),
// lives (life), wives (wife)
array('sevi', 4, false, true, 'ife'),
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
array('sev', 3, true, true, 'f'),
// axes (axis), axes (ax), axes (axe)
array('sexa', 4, false, false, array('ax', 'axe', 'axis')),
// indexes (index), matrixes (matrix)
array('sex', 3, true, false, 'x'),
// quizzes (quiz)
array('sezz', 4, true, false, 'z'),
// bureaus (bureau)
array('suae', 4, false, true, 'eau'),
// roses (rose), garages (garage), cassettes (cassette),
// waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
// shoes (shoe)
array('se', 2, true, true, array('', 'e')),
// tags (tag)
array('s', 1, true, true, ''),
// chateaux (chateau)
array('xuae', 4, false, true, 'eau'),
);
/**
* This class should not be instantiated
*/
private function __construct() {}
/**
* Returns the singular form of a word
* Alias for {@link StringUtil::singularify()}
*
* If the method can't determine the form with certainty, an array of the
* possible singulars is returned.
*
* @param string $plural A word in plural form
* @return string|array The singular form or an array of possible singular
* forms
* @deprecated Deprecated since version 2.2, to be removed in 2.3. Use
* {@link StringUtil::singularify()} instead.
*/
public static function singularify($plural)
{
$pluralRev = strrev($plural);
$lowerPluralRev = strtolower($pluralRev);
$pluralLength = strlen($lowerPluralRev);
trigger_error('\Symfony\Component\Form\Util\FormUtil::singularify() is deprecated since version 2.2 and will be removed in 2.3. Use \Symfony\Component\PropertyAccess\StringUtil::singularify() in the PropertyAccess component instead.', E_USER_DEPRECATED);
// The outer loop iterates over the entries of the plural table
// The inner loop $j iterates over the characters of the plural suffix
// in the plural table to compare them with the characters of the actual
// given plural suffix
foreach (self::$pluralMap as $map) {
$suffix = $map[0];
$suffixLength = $map[1];
$j = 0;
// Compare characters in the plural table and of the suffix of the
// given plural one by one
while ($suffix[$j] === $lowerPluralRev[$j]) {
// Let $j point to the next character
++$j;
// Successfully compared the last character
// Add an entry with the singular suffix to the singular array
if ($j === $suffixLength) {
// Is there any character preceding the suffix in the plural string?
if ($j < $pluralLength) {
$nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]);
if (!$map[2] && $nextIsVocal) {
// suffix may not succeed a vocal but next char is one
break;
}
if (!$map[3] && !$nextIsVocal) {
// suffix may not succeed a consonant but next char is one
break;
}
}
$newBase = substr($plural, 0, $pluralLength - $suffixLength);
$newSuffix = $map[4];
// Check whether the first character in the plural suffix
// is uppercased. If yes, uppercase the first character in
// the singular suffix too
$firstUpper = ctype_upper($pluralRev[$j - 1]);
if (is_array($newSuffix)) {
$singulars = array();
foreach ($newSuffix as $newSuffixEntry) {
$singulars[] = $newBase . ($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
}
return $singulars;
}
return $newBase . ($firstUpper ? ucFirst($newSuffix) : $newSuffix);
}
// Suffix is longer than word
if ($j === $pluralLength) {
break;
}
}
}
// Convert teeth to tooth, feet to foot
if (false !== ($pos = strpos($plural, 'ee'))) {
return substr_replace($plural, 'oo', $pos, 2);
}
// Assume that plural and singular is identical
return $plural;
return StringUtil::singularify($plural);
}
/**

View File

@ -11,619 +11,47 @@
namespace Symfony\Component\Form\Util;
use Traversable;
use ReflectionClass;
use Symfony\Component\Form\Exception\InvalidPropertyPathException;
use Symfony\Component\Form\Exception\InvalidPropertyException;
use Symfony\Component\Form\Exception\PropertyAccessDeniedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\PropertyPath as BasePropertyPath;
use Symfony\Component\PropertyAccess\PropertyAccess;
/**
* Allows easy traversing of a property path
* Alias for {@link \Symfony\Component\PropertyAccess\PropertyPath}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated deprecated since version 2.2, to be removed in 2.3. Use
* {@link \Symfony\Component\PropertyAccess\PropertyPath}
* instead.
*/
class PropertyPath implements \IteratorAggregate, PropertyPathInterface
class PropertyPath extends BasePropertyPath
{
/**
* Character used for separating between plural and singular of an element.
* @var string
*/
const SINGULAR_SEPARATOR = '|';
const VALUE = 0;
const IS_REF = 1;
/**
* The elements of the property path
* @var array
*/
private $elements = array();
/**
* The singular forms of the elements in the property path.
* @var array
*/
private $singulars = array();
/**
* The number of elements in the property path
* @var integer
*/
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 UnexpectedTypeException If the given path is not a string.
* @throws InvalidPropertyPathException If the syntax of the property path is not valid.
* {@inheritdoc}
*/
public function __construct($propertyPath)
{
// Can be used as copy constructor
if ($propertyPath instanceof PropertyPath) {
/* @var PropertyPath $propertyPath */
$this->elements = $propertyPath->elements;
$this->singulars = $propertyPath->singulars;
$this->length = $propertyPath->length;
$this->isIndex = $propertyPath->isIndex;
$this->pathAsString = $propertyPath->pathAsString;
parent::__construct($propertyPath);
return;
}
if (!is_string($propertyPath)) {
throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\Form\Util\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;
}
// Disabled this behaviour as the syntax is not yet final
//$pos = strpos($element, self::SINGULAR_SEPARATOR);
$pos = false;
$singular = null;
if (false !== $pos) {
$singular = substr($element, $pos + 1);
$element = substr($element, 0, $pos);
}
$this->elements[] = $element;
$this->singulars[] = $singular;
$position += strlen($matches[1]);
$remaining = $matches[4];
$pattern = '/^(\.(\w+)|\[([^\]]+)\])(.*)/';
}
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);
trigger_error('\Symfony\Component\Form\Util\PropertyPath is deprecated since version 2.2 and will be removed in 2.3. Use \Symfony\Component\PropertyAccess\PropertyPath instead.', E_USER_DEPRECATED);
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->pathAsString;
}
/**
* {@inheritdoc}
*/
public function getLength()
{
return $this->length;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
if ($this->length <= 1) {
return null;
}
$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->singulars);
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('The index ' . $index . ' is not within the property path');
}
return $this->elements[$index];
}
/**
* {@inheritdoc}
*/
public function isProperty($index)
{
if (!isset($this->isIndex[$index])) {
throw new \OutOfBoundsException('The index ' . $index . ' is not within the property path');
}
return !$this->isIndex[$index];
}
/**
* {@inheritdoc}
*/
public function isIndex($index)
{
if (!isset($this->isIndex[$index])) {
throw new \OutOfBoundsException('The index ' . $index . ' is not within the property path');
}
return $this->isIndex[$index];
}
/**
* Returns the value at the end of the property path of the object
*
* Example:
* <code>
* $path = new PropertyPath('child.name');
*
* echo $path->getValue($object);
* // equals echo $object->getChild()->getName();
* </code>
*
* 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
*
* @return mixed The value at the end of the property path
*
* @throws InvalidPropertyException If the property/getter does not exist
* @throws PropertyAccessDeniedException If the property/getter exists but is not public
* Alias for {@link PropertyAccessor::getValue()}
*/
public function getValue($objectOrArray)
{
$propertyValues =& $this->readPropertiesUntil($objectOrArray, $this->length - 1);
$propertyAccessor = PropertyAccess::getPropertyAccessor();
return $propertyValues[count($propertyValues) - 1][self::VALUE];
return $propertyAccessor->getValue($objectOrArray, $this);
}
/**
* Sets the value at the end of the property path of the object
*
* Example:
* <code>
* $path = new PropertyPath('child.name');
*
* echo $path->setValue($object, 'Fabien');
* // equals echo $object->getChild()->setName('Fabien');
* </code>
*
* 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 mixed $value The value to set at the end of the property path.
*
* @throws InvalidPropertyException If a property does not exist.
* @throws PropertyAccessDeniedException If a property cannot be accessed due to
* access restrictions (private or protected).
* @throws UnexpectedTypeException If a value within the path is neither object
* nor array.
* Alias for {@link PropertyAccessor::setValue()}
*/
public function setValue(&$objectOrArray, $value)
public function setValue($objectOrArray, $value)
{
$propertyValues =& $this->readPropertiesUntil($objectOrArray, $this->length - 2);
$overwrite = true;
$propertyAccessor = PropertyAccess::getPropertyAccessor();
// Add the root object to the list
array_unshift($propertyValues, array(
self::VALUE => &$objectOrArray,
self::IS_REF => true,
));
for ($i = count($propertyValues) - 1; $i >= 0; --$i) {
$objectOrArray =& $propertyValues[$i][self::VALUE];
if ($overwrite) {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, 'object or array');
}
$property = $this->elements[$i];
$singular = $this->singulars[$i];
$isIndex = $this->isIndex[$i];
$this->writeProperty($objectOrArray, $property, $singular, $isIndex, $value);
}
$value =& $objectOrArray;
$overwrite = !$propertyValues[$i][self::IS_REF];
}
}
/**
* Reads the path from an object up to a given path index.
*
* @param object|array $objectOrArray The object or array to read from.
* @param integer $lastIndex The integer up to which should be read.
*
* @return array The values read in the path.
*
* @throws UnexpectedTypeException If a value within the path is neither object nor array.
*/
private function &readPropertiesUntil(&$objectOrArray, $lastIndex)
{
$propertyValues = array();
for ($i = 0; $i <= $lastIndex; ++$i) {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, 'object or array');
}
$property = $this->elements[$i];
$isIndex = $this->isIndex[$i];
$isArrayAccess = is_array($objectOrArray) || $objectOrArray instanceof \ArrayAccess;
// Create missing nested arrays on demand
if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) {
$objectOrArray[$property] = $i + 1 < $this->length ? array() : null;
}
$propertyValue =& $this->readProperty($objectOrArray, $property, $isIndex);
$objectOrArray =& $propertyValue[self::VALUE];
$propertyValues[] =& $propertyValue;
}
return $propertyValues;
}
/**
* Reads the a property from an object or array.
*
* @param object|array $objectOrArray The object or array to read from.
* @param string $property The property to read.
* @param Boolean $isIndex Whether to interpret the property as index.
*
* @return mixed The value of the read property
*
* @throws InvalidPropertyException If the property does not exist.
* @throws PropertyAccessDeniedException If the property cannot be accessed due to
* access restrictions (private or protected).
*/
private function &readProperty(&$objectOrArray, $property, $isIndex)
{
// Use an array instead of an object since performance is
// very crucial here
$result = array(
self::VALUE => null,
self::IS_REF => false
);
if ($isIndex) {
if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) {
throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
}
if (isset($objectOrArray[$property])) {
if (is_array($objectOrArray)) {
$result[self::VALUE] =& $objectOrArray[$property];
$result[self::IS_REF] = true;
} else {
$result[self::VALUE] = $objectOrArray[$property];
}
}
} elseif (is_object($objectOrArray)) {
$camelProp = $this->camelize($property);
$reflClass = new ReflectionClass($objectOrArray);
$getter = 'get'.$camelProp;
$isser = 'is'.$camelProp;
$hasser = 'has'.$camelProp;
if ($reflClass->hasMethod($getter)) {
if (!$reflClass->getMethod($getter)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->name));
}
$result[self::VALUE] = $objectOrArray->$getter();
} elseif ($reflClass->hasMethod($isser)) {
if (!$reflClass->getMethod($isser)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->name));
}
$result[self::VALUE] = $objectOrArray->$isser();
} elseif ($reflClass->hasMethod($hasser)) {
if (!$reflClass->getMethod($hasser)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $hasser, $reflClass->name));
}
$result[self::VALUE] = $objectOrArray->$hasser();
} elseif ($reflClass->hasMethod('__get')) {
// needed to support magic method __get
$result[self::VALUE] = $objectOrArray->$property;
} elseif ($reflClass->hasProperty($property)) {
if (!$reflClass->getProperty($property)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()" or "%s()" or "%s()"?', $property, $reflClass->name, $getter, $isser, $hasser));
}
$result[self::VALUE] =& $objectOrArray->$property;
$result[self::IS_REF] = true;
} elseif (property_exists($objectOrArray, $property)) {
// needed to support \stdClass instances
$result[self::VALUE] =& $objectOrArray->$property;
$result[self::IS_REF] = true;
} else {
throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->name));
}
} else {
throw new InvalidPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
}
// Objects are always passed around by reference
if (is_object($result[self::VALUE])) {
$result[self::IS_REF] = true;
}
return $result;
}
/**
* Sets the value of the property at the given index in the path
*
* @param object|array $objectOrArray The object or array to write to.
* @param string $property The property to write.
* @param string|null $singular The singular form of the property name or null.
* @param Boolean $isIndex Whether to interpret the property as index.
* @param mixed $value The value to write.
*
* @throws InvalidPropertyException If the property does not exist.
* @throws PropertyAccessDeniedException If the property cannot be accessed due to
* access restrictions (private or protected).
*/
private function writeProperty(&$objectOrArray, $property, $singular, $isIndex, $value)
{
$adderRemoverError = null;
if ($isIndex) {
if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) {
throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
}
$objectOrArray[$property] = $value;
} elseif (is_object($objectOrArray)) {
$reflClass = new ReflectionClass($objectOrArray);
// The plural form is the last element of the property path
$plural = $this->camelize($this->elements[$this->length - 1]);
// Any of the two methods is required, but not yet known
$singulars = null !== $singular ? array($singular) : (array) FormUtil::singularify($plural);
if (is_array($value) || $value instanceof Traversable) {
$methods = $this->findAdderAndRemover($reflClass, $singulars);
if (null !== $methods) {
// At this point the add and remove methods have been found
// Use iterator_to_array() instead of clone in order to prevent side effects
// see https://github.com/symfony/symfony/issues/4670
$itemsToAdd = is_object($value) ? iterator_to_array($value) : $value;
$itemToRemove = array();
$propertyValue = $this->readProperty($objectOrArray, $property, $isIndex);
$previousValue = $propertyValue[self::VALUE];
if (is_array($previousValue) || $previousValue instanceof Traversable) {
foreach ($previousValue as $previousItem) {
foreach ($value as $key => $item) {
if ($item === $previousItem) {
// Item found, don't add
unset($itemsToAdd[$key]);
// Next $previousItem
continue 2;
}
}
// Item not found, add to remove list
$itemToRemove[] = $previousItem;
}
}
foreach ($itemToRemove as $item) {
call_user_func(array($objectOrArray, $methods[1]), $item);
}
foreach ($itemsToAdd as $item) {
call_user_func(array($objectOrArray, $methods[0]), $item);
}
return;
} else {
$adderRemoverError = ', nor could adders and removers be found based on the ';
if (null === $singular) {
// $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars).' (provide a singular by suffixing the property path with "|{singular}" to override the guesser)';
$adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars);
} else {
$adderRemoverError .= 'passed singular: '.$singular;
}
}
}
$setter = 'set'.$this->camelize($property);
if ($reflClass->hasMethod($setter)) {
if (!$reflClass->getMethod($setter)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->name));
}
$objectOrArray->$setter($value);
} elseif ($reflClass->hasMethod('__set')) {
// needed to support magic method __set
$objectOrArray->$property = $value;
} elseif ($reflClass->hasProperty($property)) {
if (!$reflClass->getProperty($property)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s"%s. Maybe you should create the method "%s()"?', $property, $reflClass->name, $adderRemoverError, $setter));
}
$objectOrArray->$property = $value;
} elseif (property_exists($objectOrArray, $property)) {
// needed to support \stdClass instances
$objectOrArray->$property = $value;
} else {
throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"%s', $property, $setter, $reflClass->name, $adderRemoverError));
}
} else {
throw new InvalidPropertyException(sprintf('Cannot write property "%s" in an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
}
}
/**
* Camelizes a given string.
*
* @param string $string Some string.
*
* @return string The camelized version of the string.
*/
private function camelize($string)
{
return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $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.
*
* @throws InvalidPropertyException If the property does not exist.
*/
private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
{
foreach ($singulars as $singular) {
$addMethod = 'add' . $singular;
$removeMethod = 'remove' . $singular;
$addMethodFound = $this->isAccessible($reflClass, $addMethod, 1);
$removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1);
if ($addMethodFound && $removeMethodFound) {
return array($addMethod, $removeMethod);
}
if ($addMethodFound xor $removeMethodFound) {
throw new InvalidPropertyException(sprintf(
'Found the public method "%s", but did not find a public "%s" on class %s',
$addMethodFound ? $addMethod : $removeMethod,
$addMethodFound ? $removeMethod : $addMethod,
$reflClass->name
));
}
}
return null;
}
/**
* Returns whether a method is public and has a specific number of required parameters.
*
* @param \ReflectionClass $class The class of the method.
* @param string $methodName The method name.
* @param integer $parameters The number of parameters.
*
* @return Boolean Whether the method is public and has $parameters
* required parameters.
*/
private function isAccessible(ReflectionClass $class, $methodName, $parameters)
{
if ($class->hasMethod($methodName)) {
$method = $class->getMethod($methodName);
if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) {
return true;
}
}
return false;
return $propertyAccessor->getValue($objectOrArray, $this, $value);
}
}

View File

@ -11,284 +11,26 @@
namespace Symfony\Component\Form\Util;
use Symfony\Component\PropertyAccess\PropertyPathBuilder as BasePropertyPathBuilder;
/**
* Alias for {@link \Symfony\Component\PropertyAccess\PropertyPathBuilder}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated deprecated since version 2.2, to be removed in 2.3. Use
* {@link \Symfony\Component\PropertyAccess\PropertyPathBuilder}
* instead.
*/
class PropertyPathBuilder
class PropertyPathBuilder extends BasePropertyPathBuilder
{
/**
* @var array
* {@inheritdoc}
*/
private $elements = array();
/**
* @var array
*/
private $isIndex = array();
/**
* Creates a new property path builder.
*
* @param null|PropertyPathInterface $path The path to initially store
* in the builder. Optional.
*/
public function __construct(PropertyPathInterface $path = null)
public function __construct($propertyPath)
{
if (null !== $path) {
$this->append($path);
}
}
parent::__construct($propertyPath);
/**
* Appends a (sub-) path to the current path.
*
* @param PropertyPathInterface $path The path to append.
* @param integer $offset The offset where the appended piece
* starts in $path.
* @param integer $length The length of the appended piece.
* If 0, the full path is appended.
*/
public function append(PropertyPathInterface $path, $offset = 0, $length = 0)
{
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 integer $offset The offset at which to remove.
* @param integer $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('The offset ' . $offset . ' is not within the property path');
}
$this->resize($offset, $length, 0);
}
/**
* Replaces a sub-path by a different (sub-) path.
*
* @param integer $offset The offset at which to replace.
* @param integer $length The length of the piece to replace.
* @param PropertyPathInterface $path The path to insert.
* @param integer $pathOffset The offset where the inserted piece
* starts in $path.
* @param integer $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, PropertyPathInterface $path, $pathOffset = 0, $pathLength = 0)
{
if (!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);
}
}
/**
* Replaces a property element by an index element.
*
* @param integer $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('The offset ' . $offset . ' is not within the property path');
}
if (null !== $name) {
$this->elements[$offset] = $name;
}
$this->isIndex[$offset] = true;
}
/**
* Replaces an index element by a property element.
*
* @param integer $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('The offset ' . $offset . ' is not within the property path');
}
if (null !== $name) {
$this->elements[$offset] = $name;
}
$this->isIndex[$offset] = false;
}
/**
* Returns the length of the current path.
*
* @return integer 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 integer $offset The offset where the removed chunk starts.
* @param integer $cutLength The length of the removed chunk.
* @param integer $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]);
unset($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];
}
}
trigger_error('\Symfony\Component\Form\Util\PropertyPathBuilder is deprecated since version 2.2 and will be removed in 2.3. Use \Symfony\Component\PropertyAccess\PropertyPathBuilder instead.', E_USER_DEPRECATED);
}
}

View File

@ -11,74 +11,17 @@
namespace Symfony\Component\Form\Util;
use Symfony\Component\PropertyAccess\PropertyPathInterface as BasePropertyPathInterface;
/**
* Alias for {@link \Symfony\Component\PropertyAccess\PropertyPathInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated deprecated since version 2.2, to be removed in 2.3. Use
* {@link \Symfony\Component\PropertyAccess\PropertyPathInterface}
* instead.
*/
interface PropertyPathInterface extends \Traversable
interface PropertyPathInterface extends BasePropertyPathInterface
{
/**
* 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 integer 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 integer $index The index key
*
* @return string A property or index name
*
* @throws \OutOfBoundsException If the offset is invalid.
*/
public function getElement($index);
/**
* Returns whether the element at the given index is a property
*
* @param integer $index The index in the property path
*
* @return Boolean Whether the element at this index is a property
*
* @throws \OutOfBoundsException If the offset is invalid.
*/
public function isProperty($index);
/**
* Returns whether the element at the given index is an array index
*
* @param integer $index The index in the property path
*
* @return Boolean Whether the element at this index is an array index
*
* @throws \OutOfBoundsException If the offset is invalid.
*/
public function isIndex($index);
}

View File

@ -11,45 +11,26 @@
namespace Symfony\Component\Form\Util;
use Symfony\Component\PropertyAccess\PropertyPathIterator as BasePropertyPathIterator;
/**
* Traverses a property path and provides additional methods to find out
* information about the current element
* Alias for {@link \Symfony\Component\PropertyAccess\PropertyPathIterator}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated deprecated since version 2.2, to be removed in 2.3. Use
* {@link \Symfony\Component\PropertyAccess\PropertyPathIterator}
* instead.
*/
class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface
class PropertyPathIterator extends BasePropertyPathIterator
{
/**
* The traversed property path
* @var PropertyPathInterface
*/
protected $path;
/**
* Constructor.
*
* @param PropertyPathInterface $path The property path to traverse
*/
public function __construct(PropertyPathInterface $path)
{
parent::__construct($path->getElements());
$this->path = $path;
}
/**
* {@inheritdoc}
*/
public function isIndex()
public function __construct($propertyPath)
{
return $this->path->isIndex($this->key());
}
parent::__construct($propertyPath);
/**
* {@inheritdoc}
*/
public function isProperty()
{
return $this->path->isProperty($this->key());
trigger_error('\Symfony\Component\Form\Util\PropertyPathIterator is deprecated since version 2.2 and will be removed in 2.3. Use \Symfony\Component\PropertyAccess\PropertyPathIterator instead.', E_USER_DEPRECATED);
}
}

View File

@ -11,24 +11,17 @@
namespace Symfony\Component\Form\Util;
/**
* @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 Boolean
*/
public function isIndex();
use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface as BasePropertyPathIteratorInterface;
/**
* Returns whether the current element in the property path is a property
* name.
*
* @return Boolean
*/
public function isProperty();
/**
* Alias for {@link \Symfony\Component\PropertyAccess\PropertyPathIteratorInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated deprecated since version 2.2, to be removed in 2.3. Use
* {@link \Symfony\Component\PropertyAccess\PropertyPathIterator}
* instead.
*/
interface PropertyPathIteratorInterface extends BasePropertyPathIteratorInterface
{
}

View File

@ -19,7 +19,8 @@
"php": ">=5.3.3",
"symfony/event-dispatcher": "2.2.*",
"symfony/locale": "2.2.*",
"symfony/options-resolver": "2.2.*"
"symfony/options-resolver": "2.2.*",
"symfony/property-access": "2.2.*"
},
"require-dev": {
"symfony/validator": "2.2.*",

View File

@ -0,0 +1,2 @@
/Tests export-ignore
phpunit.xml.dist export-ignore

View File

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

View File

@ -9,8 +9,13 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Exception;
namespace Symfony\Component\PropertyAccess\Exception;
class InvalidPropertyException extends Exception
/**
* Marker interface for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ExceptionInterface
{
}

View File

@ -9,8 +9,13 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Exception;
namespace Symfony\Component\PropertyAccess\Exception;
class PropertyAccessDeniedException extends Exception
/**
* Thrown when a property path is malformed.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class InvalidPropertyPathException extends RuntimeException
{
}

View File

@ -9,8 +9,13 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Exception;
namespace Symfony\Component\PropertyAccess\Exception;
class InvalidPropertyPathException extends Exception
/**
* Thrown when a property cannot be found.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NoSuchPropertyException 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;
/**
* 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;
/**
* Thrown when a property cannot be accessed because it is not public.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyAccessDeniedException 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;
/**
* Base RuntimeException for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,25 @@
<?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 value does not match an expected type.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UnexpectedTypeException extends RuntimeException
{
public function __construct($value, $expectedType)
{
parent::__construct(sprintf('Expected argument of type "%s", "%s" given', $expectedType, is_object($value) ? get_class($value) : gettype($value)));
}
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2013 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,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;
/**
* 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 The new property accessor.
*/
public static function getPropertyAccessor()
{
return new PropertyAccessor();
}
/**
* This class cannot be instantiated.
*/
private function __construct()
{
}
}

View File

@ -0,0 +1,399 @@
<?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\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
/**
* Default implementation of {@link PropertyAccessorInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyAccessor implements PropertyAccessorInterface
{
const VALUE = 0;
const IS_REF = 1;
/**
* Should not be used by application code. Use
* {@link PropertyAccess::getPropertyAccessor()} instead.
*/
public function __construct()
{
}
/**
* {@inheritdoc}
*/
public function getValue($objectOrArray, $propertyPath)
{
if (is_string($propertyPath)) {
$propertyPath = new PropertyPath($propertyPath);
} elseif (!$propertyPath instanceof PropertyPathInterface) {
throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface');
}
$propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1);
return $propertyValues[count($propertyValues) - 1][self::VALUE];
}
/**
* {@inheritdoc}
*/
public function setValue(&$objectOrArray, $propertyPath, $value)
{
if (is_string($propertyPath)) {
$propertyPath = new PropertyPath($propertyPath);
} elseif (!$propertyPath instanceof PropertyPathInterface) {
throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface');
}
$propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 2);
$overwrite = true;
// Add the root object to the list
array_unshift($propertyValues, array(
self::VALUE => &$objectOrArray,
self::IS_REF => true,
));
for ($i = count($propertyValues) - 1; $i >= 0; --$i) {
$objectOrArray =& $propertyValues[$i][self::VALUE];
if ($overwrite) {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, 'object or array');
}
$property = $propertyPath->getElement($i);
//$singular = $propertyPath->singulars[$i];
$singular = null;
$isIndex = $propertyPath->isIndex($i);
$this->writeProperty($objectOrArray, $propertyPath, $property, $singular, $isIndex, $value);
}
$value =& $objectOrArray;
$overwrite = !$propertyValues[$i][self::IS_REF];
}
}
/**
* Reads the path from an object up to a given path index.
*
* @param object|array $objectOrArray The object or array to read from.
* @param PropertyPathInterface $propertyPath The property path to read.
* @param integer $lastIndex The integer up to which should be read.
*
* @return array The values read in the path.
*
* @throws UnexpectedTypeException If a value within the path is neither object nor array.
*/
private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex)
{
$propertyValues = array();
for ($i = 0; $i <= $lastIndex; ++$i) {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, 'object or array');
}
$property = $propertyPath->getElement($i);
$isIndex = $propertyPath->isIndex($i);
$isArrayAccess = is_array($objectOrArray) || $objectOrArray instanceof \ArrayAccess;
// Create missing nested arrays on demand
if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) {
$objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null;
}
$propertyValue =& $this->readProperty($objectOrArray, $propertyPath, $property, $isIndex);
$objectOrArray =& $propertyValue[self::VALUE];
$propertyValues[] =& $propertyValue;
}
return $propertyValues;
}
/**
* Reads the a property from an object or array.
*
* @param object|array $objectOrArray The object or array to read from.
* @param PropertyPathInterface $propertyPath The property path to read.
* @param string $property The property to read.
* @param Boolean $isIndex Whether to interpret the property as index.
*
* @return mixed The value of the read property
*
* @throws NoSuchPropertyException If the property does not exist.
* @throws PropertyAccessDeniedException If the property cannot be accessed due to
* access restrictions (private or protected).
*/
private function &readProperty(&$objectOrArray, PropertyPathInterface $propertyPath, $property, $isIndex)
{
// Use an array instead of an object since performance is
// very crucial here
$result = array(
self::VALUE => null,
self::IS_REF => false
);
if ($isIndex) {
if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) {
throw new NoSuchPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
}
if (isset($objectOrArray[$property])) {
if (is_array($objectOrArray)) {
$result[self::VALUE] =& $objectOrArray[$property];
$result[self::IS_REF] = true;
} else {
$result[self::VALUE] = $objectOrArray[$property];
}
}
} elseif (is_object($objectOrArray)) {
$camelProp = $this->camelize($property);
$reflClass = new \ReflectionClass($objectOrArray);
$getter = 'get'.$camelProp;
$isser = 'is'.$camelProp;
$hasser = 'has'.$camelProp;
if ($reflClass->hasMethod($getter)) {
if (!$reflClass->getMethod($getter)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->name));
}
$result[self::VALUE] = $objectOrArray->$getter();
} elseif ($reflClass->hasMethod($isser)) {
if (!$reflClass->getMethod($isser)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->name));
}
$result[self::VALUE] = $objectOrArray->$isser();
} elseif ($reflClass->hasMethod($hasser)) {
if (!$reflClass->getMethod($hasser)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $hasser, $reflClass->name));
}
$result[self::VALUE] = $objectOrArray->$hasser();
} elseif ($reflClass->hasMethod('__get')) {
// needed to support magic method __get
$result[self::VALUE] = $objectOrArray->$property;
} elseif ($reflClass->hasProperty($property)) {
if (!$reflClass->getProperty($property)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()" or "%s()" or "%s()"?', $property, $reflClass->name, $getter, $isser, $hasser));
}
$result[self::VALUE] =& $objectOrArray->$property;
$result[self::IS_REF] = true;
} elseif (property_exists($objectOrArray, $property)) {
// needed to support \stdClass instances
$result[self::VALUE] =& $objectOrArray->$property;
$result[self::IS_REF] = true;
} else {
throw new NoSuchPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->name));
}
} else {
throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
}
// Objects are always passed around by reference
if (is_object($result[self::VALUE])) {
$result[self::IS_REF] = true;
}
return $result;
}
/**
* Sets the value of the property at the given index in the path
*
* @param object|array $objectOrArray The object or array to write to.
* @param PropertyPathInterface $propertyPath The property path to write.
* @param string $property The property to write.
* @param string|null $singular The singular form of the property name or null.
* @param Boolean $isIndex Whether to interpret the property as index.
* @param mixed $value The value to write.
*
* @throws NoSuchPropertyException If the property does not exist.
* @throws PropertyAccessDeniedException If the property cannot be accessed due to
* access restrictions (private or protected).
*/
private function writeProperty(&$objectOrArray, PropertyPathInterface $propertyPath, $property, $singular, $isIndex, $value)
{
$adderRemoverError = null;
if ($isIndex) {
if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) {
throw new NoSuchPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
}
$objectOrArray[$property] = $value;
} elseif (is_object($objectOrArray)) {
$reflClass = new \ReflectionClass($objectOrArray);
// The plural form is the last element of the property path
$plural = $this->camelize($propertyPath->getElement($propertyPath->getLength() - 1));
// Any of the two methods is required, but not yet known
$singulars = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural);
if (is_array($value) || $value instanceof \Traversable) {
$methods = $this->findAdderAndRemover($reflClass, $singulars);
if (null !== $methods) {
// At this point the add and remove methods have been found
// Use iterator_to_array() instead of clone in order to prevent side effects
// see https://github.com/symfony/symfony/issues/4670
$itemsToAdd = is_object($value) ? iterator_to_array($value) : $value;
$itemToRemove = array();
$propertyValue = $this->readProperty($objectOrArray, $propertyPath, $property, $isIndex);
$previousValue = $propertyValue[self::VALUE];
if (is_array($previousValue) || $previousValue instanceof \Traversable) {
foreach ($previousValue as $previousItem) {
foreach ($value as $key => $item) {
if ($item === $previousItem) {
// Item found, don't add
unset($itemsToAdd[$key]);
// Next $previousItem
continue 2;
}
}
// Item not found, add to remove list
$itemToRemove[] = $previousItem;
}
}
foreach ($itemToRemove as $item) {
call_user_func(array($objectOrArray, $methods[1]), $item);
}
foreach ($itemsToAdd as $item) {
call_user_func(array($objectOrArray, $methods[0]), $item);
}
return;
} else {
$adderRemoverError = ', nor could adders and removers be found based on the ';
if (null === $singular) {
// $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars).' (provide a singular by suffixing the property path with "|{singular}" to override the guesser)';
$adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars);
} else {
$adderRemoverError .= 'passed singular: '.$singular;
}
}
}
$setter = 'set'.$this->camelize($property);
if ($reflClass->hasMethod($setter)) {
if (!$reflClass->getMethod($setter)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->name));
}
$objectOrArray->$setter($value);
} elseif ($reflClass->hasMethod('__set')) {
// needed to support magic method __set
$objectOrArray->$property = $value;
} elseif ($reflClass->hasProperty($property)) {
if (!$reflClass->getProperty($property)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s"%s. Maybe you should create the method "%s()"?', $property, $reflClass->name, $adderRemoverError, $setter));
}
$objectOrArray->$property = $value;
} elseif (property_exists($objectOrArray, $property)) {
// needed to support \stdClass instances
$objectOrArray->$property = $value;
} else {
throw new NoSuchPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"%s', $property, $setter, $reflClass->name, $adderRemoverError));
}
} else {
throw new NoSuchPropertyException(sprintf('Cannot write property "%s" in an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
}
}
/**
* Camelizes a given string.
*
* @param string $string Some string.
*
* @return string The camelized version of the string.
*/
private function camelize($string)
{
return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $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.
*
* @throws NoSuchPropertyException If the property does not exist.
*/
private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
{
foreach ($singulars as $singular) {
$addMethod = 'add' . $singular;
$removeMethod = 'remove' . $singular;
$addMethodFound = $this->isAccessible($reflClass, $addMethod, 1);
$removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1);
if ($addMethodFound && $removeMethodFound) {
return array($addMethod, $removeMethod);
}
if ($addMethodFound xor $removeMethodFound) {
throw new NoSuchPropertyException(sprintf(
'Found the public method "%s", but did not find a public "%s" on class %s',
$addMethodFound ? $addMethod : $removeMethod,
$addMethodFound ? $removeMethod : $addMethod,
$reflClass->name
));
}
}
return null;
}
/**
* Returns whether a method is public and has a specific number of required parameters.
*
* @param \ReflectionClass $class The class of the method.
* @param string $methodName The method name.
* @param integer $parameters The number of parameters.
*
* @return Boolean Whether the method is public and has $parameters
* required parameters.
*/
private function isAccessible(\ReflectionClass $class, $methodName, $parameters)
{
if ($class->hasMethod($methodName)) {
$method = $class->getMethod($methodName);
if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,84 @@
<?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
*
* Example:
*
* use Symfony\Component\PropertyAccess\PropertyAccess;
*
* $propertyAccessor = PropertyAccess::getPropertyAccessor();
*
* 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\NoSuchPropertyException If a property does not exist.
* @throws Exception\PropertyAccessDeniedException If a property cannot be accessed due to
* access restrictions (private or protected).
* @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
*
* Example:
*
* use Symfony\Component\PropertyAccess\PropertyAccess;
*
* $propertyAccessor = PropertyAccess::getPropertyAccessor();
*
* 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 modify.
*
* @return mixed The value at the end of the property path
*
* @throws Exception\NoSuchPropertyException If the property/getter does not exist
* @throws Exception\PropertyAccessDeniedException If the property/getter exists but is not public
*/
public function getValue($objectOrArray, $propertyPath);
}

View File

@ -0,0 +1,225 @@
<?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\InvalidPropertyPathException;
use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
/**
* 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.
* @var string
*/
const SINGULAR_SEPARATOR = '|';
/**
* The elements of the property path
* @var array
*/
private $elements = array();
/**
* The singular forms of the elements in the property path.
* @var array
*/
private $singulars = array();
/**
* The number of elements in the property path
* @var integer
*/
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 UnexpectedTypeException 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 PropertyPath) {
/* @var PropertyPath $propertyPath */
$this->elements = $propertyPath->elements;
$this->singulars = $propertyPath->singulars;
$this->length = $propertyPath->length;
$this->isIndex = $propertyPath->isIndex;
$this->pathAsString = $propertyPath->pathAsString;
return;
}
if (!is_string($propertyPath)) {
throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\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;
}
// Disabled this behaviour as the syntax is not yet final
//$pos = strpos($element, self::SINGULAR_SEPARATOR);
$pos = false;
$singular = null;
if (false !== $pos) {
$singular = substr($element, $pos + 1);
$element = substr($element, 0, $pos);
}
$this->elements[] = $element;
$this->singulars[] = $singular;
$position += strlen($matches[1]);
$remaining = $matches[4];
$pattern = '/^(\.(\w+)|\[([^\]]+)\])(.*)/';
}
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 null;
}
$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->singulars);
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('The index ' . $index . ' is not within the property path');
}
return $this->elements[$index];
}
/**
* {@inheritdoc}
*/
public function isProperty($index)
{
if (!isset($this->isIndex[$index])) {
throw new OutOfBoundsException('The index ' . $index . ' is not within the property path');
}
return !$this->isIndex[$index];
}
/**
* {@inheritdoc}
*/
public function isIndex($index)
{
if (!isset($this->isIndex[$index])) {
throw new OutOfBoundsException('The index ' . $index . ' is not within the property path');
}
return $this->isIndex[$index];
}
}

View File

@ -0,0 +1,296 @@
<?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
{
/**
* @var array
*/
private $elements = array();
/**
* @var array
*/
private $isIndex = array();
/**
* Creates a new property path builder.
*
* @param null|PropertyPathInterface $path The path to initially store
* in the builder. Optional.
*/
public function __construct(PropertyPathInterface $path = null)
{
if (null !== $path) {
$this->append($path);
}
}
/**
* Appends a (sub-) path to the current path.
*
* @param PropertyPathInterface $path The path to append.
* @param integer $offset The offset where the appended piece
* starts in $path.
* @param integer $length The length of the appended piece.
* If 0, the full path is appended.
*/
public function append(PropertyPathInterface $path, $offset = 0, $length = 0)
{
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 integer $offset The offset at which to remove.
* @param integer $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('The offset ' . $offset . ' is not within the property path');
}
$this->resize($offset, $length, 0);
}
/**
* Replaces a sub-path by a different (sub-) path.
*
* @param integer $offset The offset at which to replace.
* @param integer $length The length of the piece to replace.
* @param PropertyPathInterface $path The path to insert.
* @param integer $pathOffset The offset where the inserted piece
* starts in $path.
* @param integer $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, PropertyPathInterface $path, $pathOffset = 0, $pathLength = 0)
{
if (!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);
}
}
/**
* Replaces a property element by an index element.
*
* @param integer $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('The offset ' . $offset . ' is not within the property path');
}
if (null !== $name) {
$this->elements[$offset] = $name;
}
$this->isIndex[$offset] = true;
}
/**
* Replaces an index element by a property element.
*
* @param integer $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('The offset ' . $offset . ' is not within the property path');
}
if (null !== $name) {
$this->elements[$offset] = $name;
}
$this->isIndex[$offset] = false;
}
/**
* Returns the length of the current path.
*
* @return integer 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 integer $offset The offset where the removed chunk starts.
* @param integer $cutLength The length of the removed chunk.
* @param integer $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]);
unset($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 integer 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 integer $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 integer $index The index in the property path
*
* @return Boolean 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 integer $index The index in the property path
*
* @return Boolean 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,55 @@
<?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
{
/**
* The traversed property path
* @var PropertyPathInterface
*/
protected $path;
/**
* Constructor.
*
* @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 Boolean
*/
public function isIndex();
/**
* Returns whether the current element in the property path is a property
* name.
*
* @return Boolean
*/
public function isProperty();
}

View File

@ -0,0 +1,14 @@
PropertyAccess Component
========================
PropertyAccess reads/writes values from/to object/array graphs using a simple
string notation.
Resources
---------
You can run the unit tests with the following command:
$ cd path/to/Symfony/Component/PropertyAccess/
$ composer.phar install --dev
$ phpunit

View File

@ -0,0 +1,192 @@
<?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;
/**
* Creates singulars from plurals.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class StringUtil
{
/**
* Map english plural to singular suffixes
*
* @var array
*
* @see http://english-zone.com/spelling/plurals.html
* @see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
*/
private static $pluralMap = array(
// First entry: plural suffix, reversed
// Second entry: length of plural suffix
// Third entry: Whether the suffix may succeed a vocal
// Fourth entry: Whether the suffix may succeed a consonant
// Fifth entry: singular suffix, normal
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
array('a', 1, true, true, array('on', 'um')),
// nebulae (nebula)
array('ea', 2, true, true, 'a'),
// mice (mouse), lice (louse)
array('eci', 3, false, true, 'ouse'),
// geese (goose)
array('esee', 4, false, true, 'oose'),
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
array('i', 1, true, true, 'us'),
// men (man), women (woman)
array('nem', 3, true, true, 'man'),
// children (child)
array('nerdlihc', 8, true, true, 'child'),
// oxen (ox)
array('nexo', 4, false, false, 'ox'),
// indices (index), appendices (appendix), prices (price)
array('seci', 4, false, true, array('ex', 'ix', 'ice')),
// babies (baby)
array('sei', 3, false, true, 'y'),
// analyses (analysis), ellipses (ellipsis), funguses (fungus),
// neuroses (neurosis), theses (thesis), emphases (emphasis),
// oases (oasis), crises (crisis), houses (house), bases (base),
// atlases (atlas), kisses (kiss)
array('ses', 3, true, true, array('s', 'se', 'sis')),
// lives (life), wives (wife)
array('sevi', 4, false, true, 'ife'),
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
array('sev', 3, true, true, 'f'),
// axes (axis), axes (ax), axes (axe)
array('sexa', 4, false, false, array('ax', 'axe', 'axis')),
// indexes (index), matrixes (matrix)
array('sex', 3, true, false, 'x'),
// quizzes (quiz)
array('sezz', 4, true, false, 'z'),
// bureaus (bureau)
array('suae', 4, false, true, 'eau'),
// roses (rose), garages (garage), cassettes (cassette),
// waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
// shoes (shoe)
array('se', 2, true, true, array('', 'e')),
// tags (tag)
array('s', 1, true, true, ''),
// chateaux (chateau)
array('xuae', 4, false, true, 'eau'),
);
/**
* This class should not be instantiated
*/
private function __construct() {}
/**
* Returns the singular form of a word
*
* If the method can't determine the form with certainty, an array of the
* possible singulars is returned.
*
* @param string $plural A word in plural form
* @return string|array The singular form or an array of possible singular
* forms
*/
public static function singularify($plural)
{
$pluralRev = strrev($plural);
$lowerPluralRev = strtolower($pluralRev);
$pluralLength = strlen($lowerPluralRev);
// The outer loop iterates over the entries of the plural table
// The inner loop $j iterates over the characters of the plural suffix
// in the plural table to compare them with the characters of the actual
// given plural suffix
foreach (self::$pluralMap as $map) {
$suffix = $map[0];
$suffixLength = $map[1];
$j = 0;
// Compare characters in the plural table and of the suffix of the
// given plural one by one
while ($suffix[$j] === $lowerPluralRev[$j]) {
// Let $j point to the next character
++$j;
// Successfully compared the last character
// Add an entry with the singular suffix to the singular array
if ($j === $suffixLength) {
// Is there any character preceding the suffix in the plural string?
if ($j < $pluralLength) {
$nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]);
if (!$map[2] && $nextIsVocal) {
// suffix may not succeed a vocal but next char is one
break;
}
if (!$map[3] && !$nextIsVocal) {
// suffix may not succeed a consonant but next char is one
break;
}
}
$newBase = substr($plural, 0, $pluralLength - $suffixLength);
$newSuffix = $map[4];
// Check whether the first character in the plural suffix
// is uppercased. If yes, uppercase the first character in
// the singular suffix too
$firstUpper = ctype_upper($pluralRev[$j - 1]);
if (is_array($newSuffix)) {
$singulars = array();
foreach ($newSuffix as $newSuffixEntry) {
$singulars[] = $newBase . ($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
}
return $singulars;
}
return $newBase . ($firstUpper ? ucFirst($newSuffix) : $newSuffix);
}
// Suffix is longer than word
if ($j === $pluralLength) {
break;
}
}
}
// Convert teeth to tooth, feet to foot
if (false !== ($pos = strpos($plural, 'ee'))) {
return substr_replace($plural, 'oo', $pos, 2);
}
// Assume that plural and singular is identical
return $plural;
}
}

View File

@ -0,0 +1,71 @@
<?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 Author
{
public $firstName;
private $lastName;
private $australian;
public $child;
private $readPermissions;
private $privateProperty;
public function setLastName($lastName)
{
$this->lastName = $lastName;
}
public function getLastName()
{
return $this->lastName;
}
private function getPrivateGetter()
{
return 'foobar';
}
public function setAustralian($australian)
{
$this->australian = $australian;
}
public function isAustralian()
{
return $this->australian;
}
public function setReadPermissions($bool)
{
$this->readPermissions = $bool;
}
public function hasReadPermissions()
{
return $this->readPermissions;
}
private function isPrivateIsser()
{
return true;
}
public function getPrivateSetter()
{
}
private function setPrivateSetter($data)
{
}
}

View File

@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Fixtures;
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
class Magician
{

View File

@ -9,9 +9,9 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Util;
namespace Symfony\Component\PropertyAccess\Tests;
class PropertyPathArrayObjectTest extends PropertyPathCollectionTest
class PropertyAccessorArrayObjectTest extends PropertyAccessorCollectionTest
{
protected function getCollection(array $array)
{

View File

@ -9,9 +9,9 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Util;
namespace Symfony\Component\PropertyAccess\Tests;
class PropertyPathArrayTest extends PropertyPathCollectionTest
class PropertyAccessorArrayTest extends PropertyAccessorCollectionTest
{
protected function getCollection(array $array)
{

View File

@ -9,12 +9,13 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Util;
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\PropertyAccess\Exception\ExceptionInterface;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\StringUtil;
class PropertyPathCollectionTest_Car
class PropertyAccessorCollectionTest_Car
{
private $axes;
@ -23,7 +24,7 @@ class PropertyPathCollectionTest_Car
$this->axes = $axes;
}
// In the test, use a name that FormUtil can't uniquely singularify
// In the test, use a name that StringUtil can't uniquely singularify
public function addAxis($axis)
{
$this->axes[] = $axis;
@ -46,7 +47,7 @@ class PropertyPathCollectionTest_Car
}
}
class PropertyPathCollectionTest_CarCustomSingular
class PropertyAccessorCollectionTest_CarCustomSingular
{
public function addFoo($axis) {}
@ -55,44 +56,44 @@ class PropertyPathCollectionTest_CarCustomSingular
public function getAxes() {}
}
class PropertyPathCollectionTest_Engine
class PropertyAccessorCollectionTest_Engine
{
}
class PropertyPathCollectionTest_CarOnlyAdder
class PropertyAccessorCollectionTest_CarOnlyAdder
{
public function addAxis($axis) {}
public function getAxes() {}
}
class PropertyPathCollectionTest_CarOnlyRemover
class PropertyAccessorCollectionTest_CarOnlyRemover
{
public function removeAxis($axis) {}
public function getAxes() {}
}
class PropertyPathCollectionTest_CarNoAdderAndRemover
class PropertyAccessorCollectionTest_CarNoAdderAndRemover
{
public function getAxes() {}
}
class PropertyPathCollectionTest_CarNoAdderAndRemoverWithProperty
class PropertyAccessorCollectionTest_CarNoAdderAndRemoverWithProperty
{
protected $axes = array();
public function getAxes() {}
}
class PropertyPathCollectionTest_CompositeCar
class PropertyAccessorCollectionTest_CompositeCar
{
public function getStructure() {}
public function setStructure($structure) {}
}
class PropertyPathCollectionTest_CarStructure
class PropertyAccessorCollectionTest_CarStructure
{
public function addAxis($axis) {}
@ -101,44 +102,47 @@ class PropertyPathCollectionTest_CarStructure
public function getAxes() {}
}
abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCase
{
/**
* @var PropertyAccessor
*/
private $propertyAccessor;
protected function setUp()
{
$this->propertyAccessor = new PropertyAccessor();
}
abstract protected function getCollection(array $array);
public function testGetValueReadsArrayAccess()
{
$object = $this->getCollection(array('firstName' => 'Bernhard'));
$path = new PropertyPath('[firstName]');
$this->assertEquals('Bernhard', $path->getValue($object));
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, '[firstName]'));
}
public function testGetValueReadsNestedArrayAccess()
{
$object = $this->getCollection(array('person' => array('firstName' => 'Bernhard')));
$path = new PropertyPath('[person][firstName]');
$this->assertEquals('Bernhard', $path->getValue($object));
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, '[person][firstName]'));
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testGetValueThrowsExceptionIfArrayAccessExpected()
{
$path = new PropertyPath('[firstName]');
$path->getValue(new \stdClass());
$this->propertyAccessor->getValue(new \stdClass(), '[firstName]');
}
public function testSetValueUpdatesArrayAccess()
{
$object = $this->getCollection(array());
$path = new PropertyPath('[firstName]');
$path->setValue($object, 'Bernhard');
$this->propertyAccessor->setValue($object, '[firstName]', 'Bernhard');
$this->assertEquals('Bernhard', $object['firstName']);
}
@ -147,20 +151,17 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
{
$object = $this->getCollection(array());
$path = new PropertyPath('[person][firstName]');
$path->setValue($object, 'Bernhard');
$this->propertyAccessor->setValue($object, '[person][firstName]', 'Bernhard');
$this->assertEquals('Bernhard', $object['person']['firstName']);
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueThrowsExceptionIfArrayAccessExpected()
{
$path = new PropertyPath('[firstName]');
$path->setValue(new \stdClass(), 'Bernhard');
$this->propertyAccessor->setValue(new \stdClass(), '[firstName]', 'Bernhard');
}
public function testSetValueCallsAdderAndRemoverForCollections()
@ -172,11 +173,9 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
// Don't use a mock in order to test whether the collections are
// modified while iterating them
$car = new PropertyPathCollectionTest_Car($axesBefore);
$car = new PropertyAccessorCollectionTest_Car($axesBefore);
$path = new PropertyPath('axes');
$path->setValue($car, $axesMerged);
$this->propertyAccessor->setValue($car, 'axes', $axesMerged);
$this->assertEquals($axesAfter, $car->getAxes());
@ -191,8 +190,6 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
$axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
$path = new PropertyPath('structure.axes');
$car->expects($this->any())
->method('getStructure')
->will($this->returnValue($structure));
@ -210,7 +207,7 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
->method('addAxis')
->with('third');
$path->setValue($car, $axesAfter);
$this->propertyAccessor->setValue($car, 'structure.axes', $axesAfter);
}
public function testSetValueCallsCustomAdderAndRemover()
@ -221,8 +218,6 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
$axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
$path = new PropertyPath('axes|foo');
$car->expects($this->at(0))
->method('getAxes')
->will($this->returnValue($axesBefore));
@ -236,43 +231,39 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
->method('addFoo')
->with('third');
$path->setValue($car, $axesAfter);
$this->propertyAccessor->setValue($car, 'axes|foo', $axesAfter);
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testMapFormToDataFailsIfOnlyAdderFound()
public function testSetValueFailsIfOnlyAdderFound()
{
$car = $this->getMock(__CLASS__ . '_CarOnlyAdder');
$axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
$path = new PropertyPath('axes');
$car->expects($this->any())
->method('getAxes')
->will($this->returnValue($axesBefore));
$path->setValue($car, $axesAfter);
$this->propertyAccessor->setValue($car, 'axes', $axesAfter);
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testMapFormToDataFailsIfOnlyRemoverFound()
public function testSetValueFailsIfOnlyRemoverFound()
{
$car = $this->getMock(__CLASS__ . '_CarOnlyRemover');
$axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
$path = new PropertyPath('axes');
$car->expects($this->any())
->method('getAxes')
->will($this->returnValue($axesBefore));
$path->setValue($car, $axesAfter);
$this->propertyAccessor->setValue($car, 'axes', $axesAfter);
}
/**
@ -283,9 +274,9 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
$axes = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
try {
$path->setValue($car, $axes);
$this->propertyAccessor->setValue($car, $path, $axes);
$this->fail('An expected exception was not thrown!');
} catch (\Symfony\Component\Form\Exception\Exception $e) {
} catch (ExceptionInterface $e) {
$this->assertEquals($message, $e->getMessage());
}
}
@ -295,7 +286,7 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
$data = array();
$car = $this->getMock(__CLASS__ . '_CarNoAdderAndRemover');
$propertyPath = new PropertyPath('axes');
$propertyPath = 'axes';
$expectedMessage = sprintf(
'Neither element "axes" nor method "setAxes()" exists in class '
.'"%s", nor could adders and removers be found based on the '
@ -304,7 +295,7 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
// .'property path with "|{singular}" to override the guesser)'
,
get_class($car),
implode(', ', (array) $singulars = FormUtil::singularify('Axes'))
implode(', ', (array) $singulars = StringUtil::singularify('Axes'))
);
$data[] = array($car, $propertyPath, $expectedMessage);
@ -323,7 +314,7 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
*/
$car = $this->getMock(__CLASS__ . '_CarNoAdderAndRemoverWithProperty');
$propertyPath = new PropertyPath('axes');
$propertyPath = 'axes';
$expectedMessage = sprintf(
'Property "axes" is not public in class "%s", nor could adders and '
.'removers be found based on the guessed singulars: %s'
@ -332,7 +323,7 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase
. '. Maybe you should '
.'create the method "setAxes()"?',
get_class($car),
implode(', ', (array) $singulars = FormUtil::singularify('Axes'))
implode(', ', (array) $singulars = StringUtil::singularify('Axes'))
);
$data[] = array($car, $propertyPath, $expectedMessage);

View File

@ -9,11 +9,11 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Util;
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\Form\Tests\Fixtures\CustomArrayObject;
class PropertyPathCustomArrayObjectTest extends PropertyPathCollectionTest
class PropertyAccessorCustomArrayObjectTest extends PropertyAccessorCollectionTest
{
protected function getCollection(array $array)
{

View File

@ -0,0 +1,334 @@
<?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\PropertyAccessor;
use Symfony\Component\PropertyAccess\Tests\Fixtures\Author;
use Symfony\Component\PropertyAccess\Tests\Fixtures\Magician;
class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
{
/**
* @var PropertyAccessor
*/
private $propertyAccessor;
protected function setUp()
{
$this->propertyAccessor = new PropertyAccessor();
}
public function testGetValueReadsArray()
{
$array = array('firstName' => 'Bernhard');
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[firstName]'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testGetValueThrowsExceptionIfIndexNotationExpected()
{
$array = array('firstName' => 'Bernhard');
$this->propertyAccessor->getValue($array, 'firstName');
}
public function testGetValueReadsZeroIndex()
{
$array = array('Bernhard');
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[0]'));
}
public function testGetValueReadsIndexWithSpecialChars()
{
$array = array('%!@$§.' => 'Bernhard');
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[%!@$§.]'));
}
public function testGetValueReadsNestedIndexWithSpecialChars()
{
$array = array('root' => array('%!@$§.' => 'Bernhard'));
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[root][%!@$§.]'));
}
public function testGetValueReadsArrayWithCustomPropertyPath()
{
$array = array('child' => array('index' => array('firstName' => 'Bernhard')));
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[child][index][firstName]'));
}
public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath()
{
$array = array('child' => array('index' => array()));
$this->assertNull($this->propertyAccessor->getValue($array, '[child][index][firstName]'));
}
public function testGetValueReadsProperty()
{
$object = new Author();
$object->firstName = 'Bernhard';
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, 'firstName'));
}
public function testGetValueIgnoresSingular()
{
$this->markTestSkipped('This feature is temporarily disabled as of 2.1');
$object = (object) array('children' => 'Many');
$this->assertEquals('Many', $this->propertyAccessor->getValue($object, 'children|child'));
}
public function testGetValueReadsPropertyWithSpecialCharsExceptDot()
{
$array = (object) array('%!@$§' => 'Bernhard');
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '%!@$§'));
}
public function testGetValueReadsPropertyWithCustomPropertyPath()
{
$object = new Author();
$object->child = array();
$object->child['index'] = new Author();
$object->child['index']->firstName = 'Bernhard';
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, 'child[index].firstName'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfPropertyIsNotPublic()
{
$this->propertyAccessor->getValue(new Author(), 'privateProperty');
}
public function testGetValueReadsGetters()
{
$object = new Author();
$object->setLastName('Schussek');
$this->assertEquals('Schussek', $this->propertyAccessor->getValue($object, 'lastName'));
}
public function testGetValueCamelizesGetterNames()
{
$object = new Author();
$object->setLastName('Schussek');
$this->assertEquals('Schussek', $this->propertyAccessor->getValue($object, 'last_name'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfGetterIsNotPublic()
{
$this->propertyAccessor->getValue(new Author(), 'privateGetter');
}
public function testGetValueReadsIssers()
{
$object = new Author();
$object->setAustralian(false);
$this->assertFalse($this->propertyAccessor->getValue($object, 'australian'));
}
public function testGetValueReadHassers()
{
$object = new Author();
$object->setReadPermissions(true);
$this->assertTrue($this->propertyAccessor->getValue($object, 'read_permissions'));
}
public function testGetValueReadsMagicGet()
{
$object = new Magician();
$object->__set('magicProperty', 'foobar');
$this->assertSame('foobar', $this->propertyAccessor->getValue($object, 'magicProperty'));
}
/*
* https://github.com/symfony/symfony/pull/4450
*/
public function testGetValueReadsMagicGetThatReturnsConstant()
{
$object = new Magician();
$this->assertNull($this->propertyAccessor->getValue($object, 'magicProperty'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfIsserIsNotPublic()
{
$this->propertyAccessor->getValue(new Author(), 'privateIsser');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testGetValueThrowsExceptionIfPropertyDoesNotExist()
{
$this->propertyAccessor->getValue(new Author(), 'foobar');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfNotObjectOrArray()
{
$this->propertyAccessor->getValue('baz', 'foobar');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfNull()
{
$this->propertyAccessor->getValue(null, 'foobar');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfEmpty()
{
$this->propertyAccessor->getValue('', 'foobar');
}
public function testSetValueUpdatesArrays()
{
$array = array();
$this->propertyAccessor->setValue($array, '[firstName]', 'Bernhard');
$this->assertEquals(array('firstName' => 'Bernhard'), $array);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueThrowsExceptionIfIndexNotationExpected()
{
$array = array();
$this->propertyAccessor->setValue($array, 'firstName', 'Bernhard');
}
public function testSetValueUpdatesArraysWithCustomPropertyPath()
{
$array = array();
$this->propertyAccessor->setValue($array, '[child][index][firstName]', 'Bernhard');
$this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array);
}
public function testSetValueUpdatesProperties()
{
$object = new Author();
$this->propertyAccessor->setValue($object, 'firstName', 'Bernhard');
$this->assertEquals('Bernhard', $object->firstName);
}
public function testSetValueUpdatesPropertiesWithCustomPropertyPath()
{
$object = new Author();
$object->child = array();
$object->child['index'] = new Author();
$this->propertyAccessor->setValue($object, 'child[index].firstName', 'Bernhard');
$this->assertEquals('Bernhard', $object->child['index']->firstName);
}
public function testSetValueUpdateMagicSet()
{
$object = new Magician();
$this->propertyAccessor->setValue($object, 'magicProperty', 'foobar');
$this->assertEquals('foobar', $object->__get('magicProperty'));
}
public function testSetValueUpdatesSetters()
{
$object = new Author();
$this->propertyAccessor->setValue($object, 'lastName', 'Schussek');
$this->assertEquals('Schussek', $object->getLastName());
}
public function testSetValueCamelizesSetterNames()
{
$object = new Author();
$this->propertyAccessor->setValue($object, 'last_name', 'Schussek');
$this->assertEquals('Schussek', $object->getLastName());
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException
*/
public function testSetValueThrowsExceptionIfGetterIsNotPublic()
{
$this->propertyAccessor->setValue(new Author(), 'privateSetter', 'foobar');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfNotObjectOrArray()
{
$value = 'baz';
$this->propertyAccessor->setValue($value, 'foobar', 'bam');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfNull()
{
$value = null;
$this->propertyAccessor->setValue($value, 'foobar', 'bam');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfEmpty()
{
$value = '';
$this->propertyAccessor->setValue($value, 'foobar', 'bam');
}
}

View File

@ -9,10 +9,10 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Util;
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Util\PropertyPathBuilder;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathBuilder;
/**
* @author Bernhard Schussek <bschussek@gmail.com>

View File

@ -0,0 +1,191 @@
<?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\PropertyPath;
use Symfony\Component\PropertyAccess\Tests\Fixtures\Author;
use Symfony\Component\PropertyAccess\Tests\Fixtures\Magician;
class PropertyPathTest extends \PHPUnit_Framework_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 testInvalidPropertyPath_noDotBeforeProperty()
{
new PropertyPath('[index]property');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_dotAtTheBeginning()
{
new PropertyPath('.property');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_unexpectedCharacters()
{
new PropertyPath('property.$foo');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_empty()
{
new PropertyPath('');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
*/
public function testInvalidPropertyPath_null()
{
new PropertyPath(null);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
*/
public function testInvalidPropertyPath_false()
{
new PropertyPath(false);
}
public function testValidPropertyPath_zero()
{
new PropertyPath('0');
}
public function testGetParent_dot()
{
$propertyPath = new PropertyPath('grandpa.parent.child');
$this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent());
}
public function testGetParent_index()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent());
}
public function testGetParent_noParent()
{
$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

@ -9,11 +9,11 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Util;
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\PropertyAccess\StringUtil;
class FormUtilTest extends \PHPUnit_Framework_TestCase
class StringUtilTest extends \PHPUnit_Framework_TestCase
{
public function singularifyProvider()
{
@ -130,6 +130,6 @@ class FormUtilTest extends \PHPUnit_Framework_TestCase
*/
public function testSingularify($plural, $singular)
{
$this->assertEquals($singular, FormUtil::singularify($plural));
$this->assertEquals($singular, StringUtil::singularify($plural));
}
}

View File

@ -0,0 +1,31 @@
{
"name": "symfony/property-access",
"type": "library",
"description": "Symfony PropertyAccess Component",
"keywords": ["property", "index", "access", "object", "array", "extraction", "injection", "reflection", "property path"],
"homepage": "http://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "http://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.3"
},
"autoload": {
"psr-0": { "Symfony\\Component\\PropertyAccess\\": "" }
},
"target-dir": "Symfony/Component/PropertyAccess",
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.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>
</exclude>
</whitelist>
</filter>
</phpunit>