2010-06-24 09:40:05 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/*
|
2011-01-15 13:29:43 +00:00
|
|
|
* This file is part of the Symfony package.
|
2010-10-02 11:38:11 +01:00
|
|
|
*
|
2010-06-24 09:40:05 +01:00
|
|
|
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
|
|
|
|
*
|
2011-01-15 13:29:43 +00:00
|
|
|
* For the full copyright and license information, please view the LICENSE
|
|
|
|
* file that was distributed with this source code.
|
2010-06-24 09:40:05 +01:00
|
|
|
*/
|
|
|
|
|
2011-01-15 13:29:43 +00:00
|
|
|
namespace Symfony\Component\Form;
|
|
|
|
|
2011-01-28 22:09:22 +00:00
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
|
|
use Symfony\Component\HttpFoundation\FileBag;
|
2010-10-02 11:38:11 +01:00
|
|
|
use Symfony\Component\Validator\ValidatorInterface;
|
2011-02-03 09:53:51 +00:00
|
|
|
use Symfony\Component\Validator\ExecutionContext;
|
2010-12-15 14:36:47 +00:00
|
|
|
use Symfony\Component\Form\Exception\FormException;
|
2011-02-02 08:41:57 +00:00
|
|
|
use Symfony\Component\Form\Exception\MissingOptionsException;
|
2011-02-02 09:15:27 +00:00
|
|
|
use Symfony\Component\Form\Exception\AlreadySubmittedException;
|
2011-01-29 21:39:36 +00:00
|
|
|
use Symfony\Component\Form\Exception\UnexpectedTypeException;
|
|
|
|
use Symfony\Component\Form\Exception\DanglingFieldException;
|
|
|
|
use Symfony\Component\Form\Exception\FieldDefinitionException;
|
2011-01-25 08:56:37 +00:00
|
|
|
use Symfony\Component\Form\CsrfProvider\CsrfProviderInterface;
|
2011-02-21 22:40:12 +00:00
|
|
|
use Symfony\Component\Form\FieldFactory\FieldFactoryInterface;
|
2011-03-01 13:19:28 +00:00
|
|
|
use Symfony\Component\Form\Filter\FilterInterface;
|
2010-10-02 11:38:11 +01:00
|
|
|
|
2010-06-24 09:40:05 +01:00
|
|
|
/**
|
|
|
|
* Form represents a form.
|
|
|
|
*
|
|
|
|
* A form is composed of a validator schema and a widget form schema.
|
|
|
|
*
|
2011-01-28 22:09:22 +00:00
|
|
|
* Form also takes care of CSRF protection by default.
|
2010-06-24 09:40:05 +01:00
|
|
|
*
|
2011-01-28 22:09:22 +00:00
|
|
|
* A CSRF secret can be any random string. If set to false, it disables the
|
|
|
|
* CSRF protection, and if set to null, it forces the form to use the global
|
|
|
|
* CSRF secret. If the global CSRF secret is also null, then a random one
|
2010-06-24 09:40:05 +01:00
|
|
|
* is generated on the fly.
|
|
|
|
*
|
2010-10-17 12:45:15 +01:00
|
|
|
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
|
2011-01-19 12:43:24 +00:00
|
|
|
* @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
|
2010-06-24 09:40:05 +01:00
|
|
|
*/
|
2011-03-01 13:19:28 +00:00
|
|
|
class Form extends Field implements \IteratorAggregate, FormInterface, FilterInterface
|
2010-06-24 09:40:05 +01:00
|
|
|
{
|
2011-01-25 08:56:37 +00:00
|
|
|
/**
|
2011-01-29 21:39:36 +00:00
|
|
|
* Contains all the fields of this group
|
|
|
|
* @var array
|
2011-01-25 08:56:37 +00:00
|
|
|
*/
|
2011-02-21 22:40:12 +00:00
|
|
|
private $fields = array();
|
2011-01-29 21:39:36 +00:00
|
|
|
|
|
|
|
/**
|
2011-02-01 09:59:18 +00:00
|
|
|
* Contains the names of submitted values who don't belong to any fields
|
2011-01-29 21:39:36 +00:00
|
|
|
* @var array
|
|
|
|
*/
|
2011-02-21 22:40:12 +00:00
|
|
|
private $extraFields = array();
|
2010-06-24 10:24:08 +01:00
|
|
|
|
2011-02-01 09:59:18 +00:00
|
|
|
/**
|
|
|
|
* Stores the class that the data of this form must be instances of
|
|
|
|
* @var string
|
|
|
|
*/
|
2011-02-21 22:40:12 +00:00
|
|
|
private $dataClass;
|
2011-02-01 09:59:18 +00:00
|
|
|
|
2011-02-07 17:52:37 +00:00
|
|
|
/**
|
|
|
|
* Stores the constructor closure for creating new domain object instances
|
|
|
|
* @var \Closure
|
|
|
|
*/
|
2011-02-21 22:40:12 +00:00
|
|
|
private $dataConstructor;
|
2011-02-01 12:45:27 +00:00
|
|
|
|
2011-02-24 12:17:53 +00:00
|
|
|
private $modifyByReference = true;
|
2011-02-19 16:05:14 +00:00
|
|
|
|
2011-03-02 13:58:19 +00:00
|
|
|
private $factory;
|
|
|
|
|
2011-02-21 22:40:12 +00:00
|
|
|
private $validator;
|
2011-02-01 12:45:27 +00:00
|
|
|
|
2011-02-21 22:40:12 +00:00
|
|
|
private $validationGroups;
|
2011-01-19 12:43:24 +00:00
|
|
|
|
2011-02-21 22:40:12 +00:00
|
|
|
private $virtual;
|
2011-01-05 11:36:04 +00:00
|
|
|
|
2011-02-21 22:40:12 +00:00
|
|
|
private $csrfFieldName;
|
2011-02-01 09:59:18 +00:00
|
|
|
|
2011-02-21 22:40:12 +00:00
|
|
|
private $csrfProvider;
|
2010-06-24 10:24:08 +01:00
|
|
|
|
2011-03-02 13:58:19 +00:00
|
|
|
public function __construct($key, FormFactoryInterface $factory,
|
|
|
|
CsrfProviderInterface $csrfProvider, ValidatorInterface $validator)
|
2011-03-01 13:19:28 +00:00
|
|
|
{
|
|
|
|
parent::__construct($key);
|
|
|
|
|
2011-03-02 13:58:19 +00:00
|
|
|
$this->factory = $factory;
|
|
|
|
$this->csrfProvider = $csrfProvider;
|
|
|
|
$this->validator = $validator;
|
|
|
|
|
2011-03-01 13:19:28 +00:00
|
|
|
$this->appendFilter($this);
|
|
|
|
}
|
|
|
|
|
2011-01-29 21:39:36 +00:00
|
|
|
/**
|
|
|
|
* Clones this group
|
|
|
|
*/
|
|
|
|
public function __clone()
|
|
|
|
{
|
|
|
|
foreach ($this->fields as $name => $field) {
|
|
|
|
$field = clone $field;
|
|
|
|
// this condition is only to "bypass" a PHPUnit bug with mocks
|
|
|
|
if (null !== $field->getParent()) {
|
|
|
|
$field->setParent($this);
|
|
|
|
}
|
|
|
|
$this->fields[$name] = $field;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a new field to this group. A field must have a unique name within
|
|
|
|
* the group. Otherwise the existing field is overwritten.
|
|
|
|
*
|
|
|
|
* If you add a nested group, this group should also be represented in the
|
|
|
|
* object hierarchy. If you want to add a group that operates on the same
|
|
|
|
* hierarchy level, use merge().
|
|
|
|
*
|
|
|
|
* <code>
|
|
|
|
* class Entity
|
|
|
|
* {
|
|
|
|
* public $location;
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* class Location
|
|
|
|
* {
|
|
|
|
* public $longitude;
|
|
|
|
* public $latitude;
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* $entity = new Entity();
|
|
|
|
* $entity->location = new Location();
|
|
|
|
*
|
|
|
|
* $form = new Form('entity', $entity, $validator);
|
|
|
|
*
|
|
|
|
* $locationGroup = new Form('location');
|
|
|
|
* $locationGroup->add(new TextField('longitude'));
|
|
|
|
* $locationGroup->add(new TextField('latitude'));
|
|
|
|
*
|
|
|
|
* $form->add($locationGroup);
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* @param FieldInterface|string $field
|
|
|
|
* @return FieldInterface
|
|
|
|
*/
|
|
|
|
public function add($field)
|
|
|
|
{
|
2011-02-02 09:15:27 +00:00
|
|
|
if ($this->isSubmitted()) {
|
|
|
|
throw new AlreadySubmittedException('You cannot add fields after submitting a form');
|
2011-01-29 21:39:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// if the field is given as string, ask the field factory of the form
|
|
|
|
// to create a field
|
|
|
|
if (!$field instanceof FieldInterface) {
|
|
|
|
if (!is_string($field)) {
|
|
|
|
throw new UnexpectedTypeException($field, 'FieldInterface or string');
|
|
|
|
}
|
|
|
|
|
2011-03-02 13:58:19 +00:00
|
|
|
if (func_num_args() > 2 || (func_num_args() > 1 && !is_array(func_get_arg(1)))) {
|
|
|
|
$identifier = func_get_arg(0);
|
|
|
|
$key = func_get_arg(1);
|
|
|
|
$options = func_num_args() > 2 ? func_get_arg(2) : array();
|
2011-02-01 09:59:18 +00:00
|
|
|
|
2011-03-02 13:58:19 +00:00
|
|
|
$field = $this->factory->getInstance($identifier, $key, $options);
|
|
|
|
} else {
|
|
|
|
$class = $this->getDataClass();
|
2011-02-01 09:59:18 +00:00
|
|
|
|
2011-03-02 13:58:19 +00:00
|
|
|
if (!$class) {
|
|
|
|
throw new FormException('The data class must be set to automatically create fields');
|
|
|
|
}
|
2011-01-29 21:39:36 +00:00
|
|
|
|
2011-03-02 13:58:19 +00:00
|
|
|
$property = func_get_arg(0);
|
|
|
|
$options = func_num_args() > 1 ? func_get_arg(1) : array();
|
2011-02-22 00:06:06 +00:00
|
|
|
|
2011-03-02 13:58:19 +00:00
|
|
|
$field = $this->factory->getInstanceForProperty($class, $property, $options);
|
|
|
|
}
|
2011-01-29 21:39:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ('' === $field->getKey() || null === $field->getKey()) {
|
|
|
|
throw new FieldDefinitionException('You cannot add anonymous fields');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->fields[$field->getKey()] = $field;
|
|
|
|
|
|
|
|
$field->setParent($this);
|
|
|
|
|
|
|
|
$data = $this->getTransformedData();
|
|
|
|
|
|
|
|
// if the property "data" is NULL, getTransformedData() returns an empty
|
|
|
|
// string
|
|
|
|
if (!empty($data)) {
|
2011-02-01 09:59:18 +00:00
|
|
|
$field->readProperty($data);
|
2011-01-29 21:39:36 +00:00
|
|
|
}
|
|
|
|
|
2011-02-19 16:05:14 +00:00
|
|
|
return $this;
|
2011-01-29 21:39:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the field with the given key.
|
|
|
|
*
|
|
|
|
* @param string $key
|
|
|
|
*/
|
|
|
|
public function remove($key)
|
|
|
|
{
|
|
|
|
$this->fields[$key]->setParent(null);
|
|
|
|
|
|
|
|
unset($this->fields[$key]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether a field with the given key exists.
|
|
|
|
*
|
|
|
|
* @param string $key
|
|
|
|
* @return Boolean
|
|
|
|
*/
|
|
|
|
public function has($key)
|
|
|
|
{
|
|
|
|
return isset($this->fields[$key]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the field with the given key.
|
|
|
|
*
|
|
|
|
* @param string $key
|
|
|
|
* @return FieldInterface
|
|
|
|
*/
|
|
|
|
public function get($key)
|
|
|
|
{
|
|
|
|
if (isset($this->fields[$key])) {
|
|
|
|
return $this->fields[$key];
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new \InvalidArgumentException(sprintf('Field "%s" does not exist.', $key));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns all fields in this group
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getFields()
|
|
|
|
{
|
|
|
|
return $this->fields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initializes the field group with an object to operate on
|
|
|
|
*
|
|
|
|
* @see FieldInterface
|
|
|
|
*/
|
|
|
|
public function setData($data)
|
|
|
|
{
|
|
|
|
parent::setData($data);
|
|
|
|
|
|
|
|
// get transformed data and pass its values to child fields
|
|
|
|
$data = $this->getTransformedData();
|
|
|
|
|
|
|
|
if (!empty($data) && !is_array($data) && !is_object($data)) {
|
|
|
|
throw new \InvalidArgumentException(sprintf('Expected argument of type object or array, %s given', gettype($data)));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!empty($data)) {
|
2011-02-01 09:59:18 +00:00
|
|
|
if ($this->dataClass && !$data instanceof $this->dataClass) {
|
|
|
|
throw new FormException(sprintf('Form data should be instance of %s', $this->dataClass));
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->readObject($data);
|
2011-01-29 21:39:36 +00:00
|
|
|
}
|
2011-02-21 22:40:12 +00:00
|
|
|
|
|
|
|
return $this;
|
2011-01-29 21:39:36 +00:00
|
|
|
}
|
|
|
|
|
2011-03-01 13:19:28 +00:00
|
|
|
public function filterSetData($data)
|
2011-02-24 11:22:00 +00:00
|
|
|
{
|
2011-03-01 13:19:28 +00:00
|
|
|
if (null === $this->getValueTransformer() && null === $this->getNormalizationTransformer()) {
|
2011-02-24 11:22:00 +00:00
|
|
|
// Empty values must be converted to objects or arrays so that
|
|
|
|
// they can be read by PropertyPath in the child fields
|
2011-03-01 13:19:28 +00:00
|
|
|
if (empty($data)) {
|
2011-02-24 11:22:00 +00:00
|
|
|
if ($this->dataConstructor) {
|
|
|
|
$constructor = $this->dataConstructor;
|
2011-03-01 13:19:28 +00:00
|
|
|
$data = $constructor();
|
2011-02-24 11:22:00 +00:00
|
|
|
} else if ($this->dataClass) {
|
|
|
|
$class = $this->dataClass;
|
2011-03-01 13:19:28 +00:00
|
|
|
$data = new $class();
|
2011-02-24 11:22:00 +00:00
|
|
|
} else {
|
2011-03-01 13:19:28 +00:00
|
|
|
$data = array();
|
2011-02-24 11:22:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-03-01 13:19:28 +00:00
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function filterBoundDataFromClient($data)
|
|
|
|
{
|
|
|
|
if (!is_array($data)) {
|
|
|
|
throw new UnexpectedTypeException($data, 'array');
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($this->fields as $key => $field) {
|
|
|
|
if (!isset($data[$key])) {
|
|
|
|
$data[$key] = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($data as $key => $value) {
|
|
|
|
if ($this->has($key)) {
|
|
|
|
$this->fields[$key]->submit($value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$data = $this->getTransformedData();
|
|
|
|
|
|
|
|
$this->writeObject($data);
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSupportedFilters()
|
|
|
|
{
|
|
|
|
return array(
|
|
|
|
Filters::filterSetData,
|
|
|
|
Filters::filterBoundDataFromClient,
|
|
|
|
);
|
2011-02-24 11:22:00 +00:00
|
|
|
}
|
|
|
|
|
2011-01-29 21:39:36 +00:00
|
|
|
/**
|
|
|
|
* Returns the data of the field as it is displayed to the user.
|
|
|
|
*
|
|
|
|
* @see FieldInterface
|
|
|
|
* @return array of field name => value
|
|
|
|
*/
|
|
|
|
public function getDisplayedData()
|
|
|
|
{
|
|
|
|
$values = array();
|
|
|
|
|
|
|
|
foreach ($this->fields as $key => $field) {
|
|
|
|
$values[$key] = $field->getDisplayedData();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $values;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Binds POST data to the field, transforms and validates it.
|
|
|
|
*
|
2011-02-01 09:59:18 +00:00
|
|
|
* @param string|array $data The POST data
|
2011-01-29 21:39:36 +00:00
|
|
|
*/
|
2011-02-01 09:59:18 +00:00
|
|
|
public function submit($data)
|
2011-01-29 21:39:36 +00:00
|
|
|
{
|
2011-02-01 09:59:18 +00:00
|
|
|
// set and reverse transform the data
|
|
|
|
parent::submit($data);
|
2011-01-29 21:39:36 +00:00
|
|
|
|
|
|
|
$this->extraFields = array();
|
|
|
|
|
2011-03-01 13:19:28 +00:00
|
|
|
foreach ((array)$data as $key => $value) {
|
2011-01-29 21:39:36 +00:00
|
|
|
if (!$this->has($key)) {
|
|
|
|
$this->extraFields[] = $key;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the child fields from the properties of the given data
|
|
|
|
*
|
2011-02-01 09:59:18 +00:00
|
|
|
* This method calls readProperty() on all child fields that have a
|
2011-01-29 21:39:36 +00:00
|
|
|
* property path set. If a child field has no property path set but
|
2011-02-01 09:59:18 +00:00
|
|
|
* implements FormInterface, writeProperty() is called on its
|
2011-01-29 21:39:36 +00:00
|
|
|
* children instead.
|
|
|
|
*
|
|
|
|
* @param array|object $objectOrArray
|
|
|
|
*/
|
2011-02-01 09:59:18 +00:00
|
|
|
protected function readObject(&$objectOrArray)
|
2011-01-29 21:39:36 +00:00
|
|
|
{
|
|
|
|
$iterator = new RecursiveFieldIterator($this);
|
|
|
|
$iterator = new \RecursiveIteratorIterator($iterator);
|
|
|
|
|
|
|
|
foreach ($iterator as $field) {
|
2011-02-01 09:59:18 +00:00
|
|
|
$field->readProperty($objectOrArray);
|
2011-01-29 21:39:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates all properties of the given data from the child fields
|
|
|
|
*
|
2011-02-01 09:59:18 +00:00
|
|
|
* This method calls writeProperty() on all child fields that have a property
|
2011-01-29 21:39:36 +00:00
|
|
|
* path set. If a child field has no property path set but implements
|
2011-02-01 09:59:18 +00:00
|
|
|
* FormInterface, writeProperty() is called on its children instead.
|
2011-01-29 21:39:36 +00:00
|
|
|
*
|
|
|
|
* @param array|object $objectOrArray
|
|
|
|
*/
|
2011-02-01 09:59:18 +00:00
|
|
|
protected function writeObject(&$objectOrArray)
|
2011-01-29 21:39:36 +00:00
|
|
|
{
|
|
|
|
$iterator = new RecursiveFieldIterator($this);
|
|
|
|
$iterator = new \RecursiveIteratorIterator($iterator);
|
|
|
|
|
|
|
|
foreach ($iterator as $field) {
|
2011-02-01 09:59:18 +00:00
|
|
|
$field->writeProperty($objectOrArray);
|
2011-01-29 21:39:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-02-01 09:59:18 +00:00
|
|
|
* Processes the submitted data before it is passed to the individual fields
|
2011-01-29 21:39:36 +00:00
|
|
|
*
|
|
|
|
* The data is in the user format.
|
|
|
|
*
|
|
|
|
* @param array $data
|
|
|
|
* @return array
|
|
|
|
*/
|
2011-02-19 13:25:31 +00:00
|
|
|
protected function preprocessData($data)
|
2011-01-29 21:39:36 +00:00
|
|
|
{
|
2011-02-19 13:49:54 +00:00
|
|
|
if ($this->dataPreprocessor) {
|
|
|
|
return $this->dataPreprocessor->processData($data);
|
|
|
|
}
|
|
|
|
|
2011-01-29 21:39:36 +00:00
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
2011-02-21 22:40:12 +00:00
|
|
|
public function setVirtual($virtual)
|
|
|
|
{
|
|
|
|
$this->virtual = $virtual;
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2011-01-29 21:39:36 +00:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function isVirtual()
|
|
|
|
{
|
2011-02-21 22:40:12 +00:00
|
|
|
return $this->virtual;
|
2011-01-29 21:39:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-02-01 09:59:18 +00:00
|
|
|
* Returns whether this form was submitted with extra fields
|
2011-01-29 21:39:36 +00:00
|
|
|
*
|
|
|
|
* @return Boolean
|
|
|
|
*/
|
2011-02-01 09:59:18 +00:00
|
|
|
public function isSubmittedWithExtraFields()
|
2011-01-29 21:39:36 +00:00
|
|
|
{
|
|
|
|
// TODO: integrate the field names in the error message
|
|
|
|
return count($this->extraFields) > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether the field is valid.
|
|
|
|
*
|
|
|
|
* @return Boolean
|
|
|
|
*/
|
|
|
|
public function isValid()
|
|
|
|
{
|
|
|
|
if (!parent::isValid()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($this->fields as $field) {
|
|
|
|
if (!$field->isValid()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@inheritDoc}
|
|
|
|
*/
|
2011-02-02 14:10:06 +00:00
|
|
|
public function addError(Error $error, PropertyPathIterator $pathIterator = null)
|
2011-01-29 21:39:36 +00:00
|
|
|
{
|
|
|
|
if (null !== $pathIterator) {
|
2011-02-02 14:10:06 +00:00
|
|
|
if ($error instanceof FieldError && $pathIterator->hasNext()) {
|
2011-01-29 21:39:36 +00:00
|
|
|
$pathIterator->next();
|
|
|
|
|
|
|
|
if ($pathIterator->isProperty() && $pathIterator->current() === 'fields') {
|
|
|
|
$pathIterator->next();
|
|
|
|
}
|
|
|
|
|
2011-02-28 16:01:11 +00:00
|
|
|
if ($this->has($pathIterator->current())) {
|
2011-02-02 14:10:06 +00:00
|
|
|
$this->get($pathIterator->current())->addError($error, $pathIterator);
|
2011-01-29 21:39:36 +00:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
2011-02-02 14:10:06 +00:00
|
|
|
} else if ($error instanceof DataError) {
|
2011-01-29 21:39:36 +00:00
|
|
|
$iterator = new RecursiveFieldIterator($this);
|
|
|
|
$iterator = new \RecursiveIteratorIterator($iterator);
|
|
|
|
|
|
|
|
foreach ($iterator as $field) {
|
|
|
|
if (null !== ($fieldPath = $field->getPropertyPath())) {
|
2011-02-28 16:01:11 +00:00
|
|
|
if ($fieldPath->getElement(0) === $pathIterator->current()) {
|
2011-01-29 21:39:36 +00:00
|
|
|
if ($pathIterator->hasNext()) {
|
|
|
|
$pathIterator->next();
|
|
|
|
}
|
|
|
|
|
2011-02-02 14:10:06 +00:00
|
|
|
$field->addError($error, $pathIterator);
|
2011-01-29 21:39:36 +00:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parent::addError($error);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether the field requires a multipart form.
|
|
|
|
*
|
|
|
|
* @return Boolean
|
|
|
|
*/
|
|
|
|
public function isMultipart()
|
|
|
|
{
|
|
|
|
foreach ($this->fields as $field) {
|
|
|
|
if ($field->isMultipart()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-02-01 09:59:18 +00:00
|
|
|
* Returns true if the field exists (implements the \ArrayAccess interface).
|
2011-01-29 21:39:36 +00:00
|
|
|
*
|
2011-02-01 09:59:18 +00:00
|
|
|
* @param string $key The key of the field
|
2011-01-29 21:39:36 +00:00
|
|
|
*
|
|
|
|
* @return Boolean true if the widget exists, false otherwise
|
|
|
|
*/
|
|
|
|
public function offsetExists($key)
|
|
|
|
{
|
|
|
|
return $this->has($key);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the form field associated with the name (implements the \ArrayAccess interface).
|
|
|
|
*
|
|
|
|
* @param string $key The offset of the value to get
|
|
|
|
*
|
|
|
|
* @return Field A form field instance
|
|
|
|
*/
|
|
|
|
public function offsetGet($key)
|
|
|
|
{
|
|
|
|
return $this->get($key);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Throws an exception saying that values cannot be set (implements the \ArrayAccess interface).
|
|
|
|
*
|
|
|
|
* @param string $offset (ignored)
|
|
|
|
* @param string $value (ignored)
|
|
|
|
*
|
|
|
|
* @throws \LogicException
|
|
|
|
*/
|
|
|
|
public function offsetSet($key, $field)
|
|
|
|
{
|
|
|
|
throw new \LogicException('Use the method add() to add fields');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Throws an exception saying that values cannot be unset (implements the \ArrayAccess interface).
|
|
|
|
*
|
|
|
|
* @param string $key
|
|
|
|
*
|
|
|
|
* @throws \LogicException
|
|
|
|
*/
|
|
|
|
public function offsetUnset($key)
|
|
|
|
{
|
|
|
|
return $this->remove($key);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the iterator for this group.
|
|
|
|
*
|
|
|
|
* @return \ArrayIterator
|
|
|
|
*/
|
|
|
|
public function getIterator()
|
|
|
|
{
|
|
|
|
return new \ArrayIterator($this->fields);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the number of form fields (implements the \Countable interface).
|
|
|
|
*
|
|
|
|
* @return integer The number of embedded form fields
|
|
|
|
*/
|
|
|
|
public function count()
|
|
|
|
{
|
|
|
|
return count($this->fields);
|
|
|
|
}
|
|
|
|
|
2011-02-21 22:40:12 +00:00
|
|
|
public function setValidator(ValidatorInterface $validator)
|
2010-06-24 10:24:08 +01:00
|
|
|
{
|
2011-02-21 22:40:12 +00:00
|
|
|
$this->validator = $validator;
|
|
|
|
|
|
|
|
return $this;
|
2010-06-24 10:24:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-01-19 12:43:24 +00:00
|
|
|
* Returns the validator used by the form
|
2010-06-24 10:24:08 +01:00
|
|
|
*
|
2011-01-19 12:43:24 +00:00
|
|
|
* @return ValidatorInterface The validator instance
|
|
|
|
*/
|
|
|
|
public function getValidator()
|
|
|
|
{
|
2011-02-21 22:40:12 +00:00
|
|
|
return $this->validator;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setValidationGroups($validationGroups)
|
|
|
|
{
|
2011-02-22 00:06:06 +00:00
|
|
|
$this->validationGroups = empty($validationGroups) ? null : (array)$validationGroups;
|
2011-02-21 22:40:12 +00:00
|
|
|
|
|
|
|
return $this;
|
2011-01-19 12:43:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the validation groups validated by the form
|
|
|
|
*
|
|
|
|
* @return array A list of validation groups or null
|
2010-06-24 10:24:08 +01:00
|
|
|
*/
|
|
|
|
public function getValidationGroups()
|
|
|
|
{
|
2011-02-21 22:40:12 +00:00
|
|
|
$groups = $this->validationGroups;
|
2011-02-03 09:53:51 +00:00
|
|
|
|
|
|
|
if (!$groups && $this->hasParent()) {
|
|
|
|
$groups = $this->getParent()->getValidationGroups();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $groups;
|
2010-06-24 10:24:08 +01:00
|
|
|
}
|
|
|
|
|
2011-02-21 22:40:12 +00:00
|
|
|
public function enableCsrfProtection(CsrfProviderInterface $provider, $fieldName = '_token')
|
|
|
|
{
|
|
|
|
$this->csrfProvider = $provider;
|
|
|
|
$this->csrfFieldName = $fieldName;
|
|
|
|
|
|
|
|
$token = $provider->generateCsrfToken(get_class($this));
|
|
|
|
|
2011-03-02 14:01:04 +00:00
|
|
|
$this->add('hidden', $fieldName, array(
|
|
|
|
'data' => $token,
|
|
|
|
'property_path' => null,
|
|
|
|
));
|
2011-02-21 22:40:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function disableCsrfProtection()
|
|
|
|
{
|
|
|
|
if ($this->isCsrfProtected()) {
|
|
|
|
$this->remove($this->csrfFieldName);
|
|
|
|
$this->csrfProvider = null;
|
|
|
|
$this->csrfFieldName = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-01-05 11:36:04 +00:00
|
|
|
/**
|
2011-01-19 12:43:24 +00:00
|
|
|
* Returns the name used for the CSRF protection field
|
2011-01-05 11:36:04 +00:00
|
|
|
*
|
2011-01-19 12:43:24 +00:00
|
|
|
* @return string The field name
|
2011-01-05 11:36:04 +00:00
|
|
|
*/
|
2011-01-19 12:43:24 +00:00
|
|
|
public function getCsrfFieldName()
|
2011-01-05 11:36:04 +00:00
|
|
|
{
|
2011-02-21 22:40:12 +00:00
|
|
|
return $this->csrfFieldName;
|
2011-01-19 12:43:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-01-25 08:56:37 +00:00
|
|
|
* Returns the provider used for generating and validating CSRF tokens
|
2011-01-19 12:43:24 +00:00
|
|
|
*
|
2011-01-25 08:56:37 +00:00
|
|
|
* @return CsrfProviderInterface The provider instance
|
2011-01-19 12:43:24 +00:00
|
|
|
*/
|
2011-01-25 08:56:37 +00:00
|
|
|
public function getCsrfProvider()
|
2011-01-19 12:43:24 +00:00
|
|
|
{
|
2011-02-21 22:40:12 +00:00
|
|
|
return $this->csrfProvider;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return true if this form is CSRF protected
|
|
|
|
*/
|
|
|
|
public function isCsrfProtected()
|
|
|
|
{
|
|
|
|
return $this->csrfFieldName && $this->has($this->csrfFieldName);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether the CSRF token is valid
|
|
|
|
*
|
|
|
|
* @return Boolean
|
|
|
|
*/
|
|
|
|
public function isCsrfTokenValid()
|
|
|
|
{
|
|
|
|
if (!$this->isCsrfProtected()) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
$token = $this->get($this->csrfFieldName)->getDisplayedData();
|
|
|
|
|
|
|
|
return $this->csrfProvider->isCsrfTokenValid(get_class($this), $token);
|
|
|
|
}
|
2011-01-05 11:36:04 +00:00
|
|
|
}
|
|
|
|
|
2010-06-24 10:24:08 +01:00
|
|
|
/**
|
2011-02-01 09:59:18 +00:00
|
|
|
* Binds a request to the form
|
2010-06-24 10:24:08 +01:00
|
|
|
*
|
2011-02-01 09:59:18 +00:00
|
|
|
* If the request was a POST request, the data is submitted to the form,
|
|
|
|
* transformed and written into the form data (an object or an array).
|
|
|
|
* You can set the form data by passing it in the second parameter
|
|
|
|
* of this method or by passing it in the "data" option of the form's
|
|
|
|
* constructor.
|
|
|
|
*
|
|
|
|
* @param Request $request The request to bind to the form
|
|
|
|
* @param array|object $data The data from which to read default values
|
|
|
|
* and where to write submitted values
|
2011-01-28 22:09:22 +00:00
|
|
|
*/
|
2011-02-01 09:59:18 +00:00
|
|
|
public function bind(Request $request, $data = null)
|
2011-01-28 22:09:22 +00:00
|
|
|
{
|
2011-02-19 12:46:32 +00:00
|
|
|
if (!$this->getKey()) {
|
2011-02-06 17:10:50 +00:00
|
|
|
throw new FormException('You cannot bind anonymous forms. Please give this form a name');
|
|
|
|
}
|
|
|
|
|
2011-02-01 09:59:18 +00:00
|
|
|
// Store object from which to read the default values and where to
|
|
|
|
// write the submitted values
|
|
|
|
if (null !== $data) {
|
|
|
|
$this->setData($data);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store the submitted data in case of a post request
|
|
|
|
if ('POST' == $request->getMethod()) {
|
|
|
|
$values = $request->request->get($this->getName(), array());
|
|
|
|
$files = $request->files->get($this->getName(), array());
|
2011-01-28 22:09:22 +00:00
|
|
|
|
2011-02-01 09:59:18 +00:00
|
|
|
$this->submit(self::deepArrayUnion($values, $files));
|
|
|
|
|
2011-02-02 08:41:57 +00:00
|
|
|
$this->validate();
|
2011-02-01 09:59:18 +00:00
|
|
|
}
|
2011-01-28 22:09:22 +00:00
|
|
|
}
|
|
|
|
|
2011-02-19 12:46:32 +00:00
|
|
|
/**
|
|
|
|
* @deprecated
|
|
|
|
*/
|
|
|
|
private function getName()
|
|
|
|
{
|
|
|
|
return null === $this->getParent() ? $this->getKey() : $this->getParent()->getName().'['.$this->key.']';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @deprecated
|
|
|
|
*/
|
|
|
|
private function getId()
|
|
|
|
{
|
|
|
|
return null === $this->getParent() ? $this->getKey() : $this->getParent()->getId().'_'.$this->key;
|
|
|
|
}
|
|
|
|
|
2011-02-01 09:59:18 +00:00
|
|
|
/**
|
2011-02-02 08:41:57 +00:00
|
|
|
* Validates the form and its domain object
|
|
|
|
*
|
|
|
|
* @throws FormException If the option "validator" was not set
|
2011-02-01 09:59:18 +00:00
|
|
|
*/
|
2011-02-02 08:41:57 +00:00
|
|
|
public function validate()
|
2011-02-01 09:59:18 +00:00
|
|
|
{
|
2011-02-22 00:06:06 +00:00
|
|
|
if (null === $this->validator) {
|
|
|
|
throw new MissingOptionsException('A validator is required for validating', array('validator'));
|
2011-02-02 08:41:57 +00:00
|
|
|
}
|
|
|
|
|
2011-02-03 09:53:51 +00:00
|
|
|
// Validate the form in group "Default"
|
|
|
|
// Validation of the data in the custom group is done by validateData(),
|
|
|
|
// which is constrained by the Execute constraint
|
2011-02-22 00:06:06 +00:00
|
|
|
if ($violations = $this->validator->validate($this)) {
|
2011-02-02 14:10:06 +00:00
|
|
|
foreach ($violations as $violation) {
|
|
|
|
$propertyPath = new PropertyPath($violation->getPropertyPath());
|
|
|
|
$iterator = $propertyPath->getIterator();
|
2011-02-03 09:53:51 +00:00
|
|
|
$template = $violation->getMessageTemplate();
|
|
|
|
$parameters = $violation->getMessageParameters();
|
|
|
|
|
|
|
|
if ($iterator->current() == 'data') {
|
|
|
|
$iterator->next(); // point at the first data element
|
|
|
|
$error = new DataError($template, $parameters);
|
|
|
|
} else {
|
|
|
|
$error = new FieldError($template, $parameters);
|
|
|
|
}
|
2011-02-02 08:41:57 +00:00
|
|
|
|
2011-02-02 14:10:06 +00:00
|
|
|
$this->addError($error, $iterator);
|
2011-02-02 08:41:57 +00:00
|
|
|
}
|
|
|
|
}
|
2011-01-28 22:09:22 +00:00
|
|
|
}
|
2010-09-09 14:23:28 +01:00
|
|
|
|
2010-06-24 10:24:08 +01:00
|
|
|
/**
|
|
|
|
* Returns whether the maximum POST size was reached in this request.
|
|
|
|
*
|
2011-01-21 01:39:28 +00:00
|
|
|
* @return Boolean
|
2010-06-24 10:24:08 +01:00
|
|
|
*/
|
|
|
|
public function isPostMaxSizeReached()
|
|
|
|
{
|
2011-01-28 22:13:10 +00:00
|
|
|
if ($this->isRoot() && isset($_SERVER['CONTENT_LENGTH'])) {
|
2010-06-24 10:24:08 +01:00
|
|
|
$length = (int) $_SERVER['CONTENT_LENGTH'];
|
|
|
|
$max = trim(ini_get('post_max_size'));
|
|
|
|
|
|
|
|
switch (strtolower(substr($max, -1))) {
|
|
|
|
// The 'G' modifier is available since PHP 5.1.0
|
|
|
|
case 'g':
|
|
|
|
$max *= 1024;
|
|
|
|
case 'm':
|
|
|
|
$max *= 1024;
|
|
|
|
case 'k':
|
|
|
|
$max *= 1024;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $length > $max;
|
|
|
|
}
|
2011-02-27 17:27:46 +00:00
|
|
|
|
|
|
|
return false;
|
2010-06-24 10:24:08 +01:00
|
|
|
}
|
|
|
|
|
2011-02-01 09:59:18 +00:00
|
|
|
/**
|
|
|
|
* Sets the class that object bound to this form must be instances of
|
|
|
|
*
|
|
|
|
* @param string A fully qualified class name
|
|
|
|
*/
|
2011-02-21 22:40:12 +00:00
|
|
|
public function setDataClass($class)
|
2011-02-01 09:59:18 +00:00
|
|
|
{
|
|
|
|
$this->dataClass = $class;
|
2011-02-21 22:40:12 +00:00
|
|
|
|
|
|
|
return $this;
|
2011-02-01 09:59:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the class that object must have that are bound to this form
|
|
|
|
*
|
|
|
|
* @return string A fully qualified class name
|
|
|
|
*/
|
|
|
|
public function getDataClass()
|
|
|
|
{
|
|
|
|
return $this->dataClass;
|
|
|
|
}
|
|
|
|
|
2011-02-21 22:40:12 +00:00
|
|
|
public function setDataConstructor($dataConstructor)
|
|
|
|
{
|
|
|
|
$this->dataConstructor = $dataConstructor;
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getDataConstructor()
|
|
|
|
{
|
|
|
|
return $this->dataConstructor;
|
|
|
|
}
|
|
|
|
|
2011-02-03 09:53:51 +00:00
|
|
|
/**
|
|
|
|
* Validates the data of this form
|
|
|
|
*
|
|
|
|
* This method is called automatically during the validation process.
|
|
|
|
*
|
|
|
|
* @param ExecutionContext $context The current validation context
|
|
|
|
*/
|
|
|
|
public function validateData(ExecutionContext $context)
|
|
|
|
{
|
2011-02-04 09:12:11 +00:00
|
|
|
if (is_object($this->getData()) || is_array($this->getData())) {
|
|
|
|
$groups = $this->getValidationGroups();
|
|
|
|
$propertyPath = $context->getPropertyPath();
|
|
|
|
$graphWalker = $context->getGraphWalker();
|
2011-02-03 09:53:51 +00:00
|
|
|
|
2011-02-04 09:12:11 +00:00
|
|
|
if (null === $groups) {
|
|
|
|
$groups = array(null);
|
|
|
|
}
|
2011-02-03 09:53:51 +00:00
|
|
|
|
2011-02-04 09:12:11 +00:00
|
|
|
// The Execute constraint is called on class level, so we need to
|
|
|
|
// set the property manually
|
|
|
|
$context->setCurrentProperty('data');
|
2011-02-03 09:53:51 +00:00
|
|
|
|
2011-02-04 09:12:11 +00:00
|
|
|
// Adjust the property path accordingly
|
|
|
|
if (!empty($propertyPath)) {
|
|
|
|
$propertyPath .= '.';
|
|
|
|
}
|
2011-02-03 09:53:51 +00:00
|
|
|
|
2011-02-04 09:12:11 +00:00
|
|
|
$propertyPath .= 'data';
|
2011-02-03 09:53:51 +00:00
|
|
|
|
2011-02-04 09:12:11 +00:00
|
|
|
foreach ($groups as $group) {
|
|
|
|
$graphWalker->walkReference($this->getData(), $group, $propertyPath, true);
|
|
|
|
}
|
2011-02-03 09:53:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-02-06 17:04:20 +00:00
|
|
|
/**
|
|
|
|
* {@inheritDoc}
|
|
|
|
*/
|
|
|
|
public function writeProperty(&$objectOrArray)
|
|
|
|
{
|
2011-02-09 00:15:49 +00:00
|
|
|
$isReference = false;
|
2011-02-06 17:04:20 +00:00
|
|
|
|
2011-02-09 00:15:49 +00:00
|
|
|
// If the data is identical to the value in $objectOrArray, we are
|
|
|
|
// dealing with a reference
|
|
|
|
if ($this->getPropertyPath() !== null) {
|
|
|
|
$isReference = $this->getData() === $this->getPropertyPath()->getValue($objectOrArray);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't write into $objectOrArray if $objectOrArray is an object,
|
|
|
|
// $isReference is true (see above) and the option "by_reference" is
|
|
|
|
// true as well
|
2011-02-19 16:05:14 +00:00
|
|
|
if (!is_object($objectOrArray) || !$isReference || !$this->modifyByReference) {
|
2011-02-06 17:04:20 +00:00
|
|
|
parent::writeProperty($objectOrArray);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-02-16 09:28:33 +00:00
|
|
|
/**
|
|
|
|
* {@inheritDoc}
|
|
|
|
*/
|
|
|
|
public function isEmpty()
|
|
|
|
{
|
|
|
|
foreach ($this->fields as $field) {
|
|
|
|
if (!$field->isEmpty()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2011-02-19 16:05:14 +00:00
|
|
|
public function setModifyByReference($modifyByReference)
|
|
|
|
{
|
|
|
|
$this->modifyByReference = $modifyByReference;
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function isModifiedByReference()
|
|
|
|
{
|
|
|
|
return $this->modifyByReference;
|
|
|
|
}
|
|
|
|
|
2010-06-24 10:24:08 +01:00
|
|
|
/**
|
|
|
|
* Merges two arrays without reindexing numeric keys.
|
|
|
|
*
|
|
|
|
* @param array $array1 An array to merge
|
|
|
|
* @param array $array2 An array to merge
|
|
|
|
*
|
|
|
|
* @return array The merged array
|
|
|
|
*/
|
|
|
|
static protected function deepArrayUnion($array1, $array2)
|
|
|
|
{
|
|
|
|
foreach ($array2 as $key => $value) {
|
|
|
|
if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
|
|
|
|
$array1[$key] = self::deepArrayUnion($array1[$key], $value);
|
|
|
|
} else {
|
|
|
|
$array1[$key] = $value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $array1;
|
|
|
|
}
|
2010-06-24 09:40:05 +01:00
|
|
|
}
|