123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856 |
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\HttpFoundation\Request;
- use Symfony\Component\HttpFoundation\FileBag;
- use Symfony\Component\Validator\ValidatorInterface;
- use Symfony\Component\Form\Exception\FormException;
- use Symfony\Component\Form\Exception\MissingOptionsException;
- use Symfony\Component\Form\Exception\AlreadySubmittedException;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- use Symfony\Component\Form\Exception\DanglingFieldException;
- use Symfony\Component\Form\Exception\FieldDefinitionException;
- use Symfony\Component\Form\CsrfProvider\CsrfProviderInterface;
- /**
- * Form represents a form.
- *
- * A form is composed of a validator schema and a widget form schema.
- *
- * Form also takes care of CSRF protection by default.
- *
- * 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
- * is generated on the fly.
- *
- * @author Fabien Potencier <fabien.potencier@symfony-project.com>
- * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
- */
- class Form extends Field implements \IteratorAggregate, FormInterface
- {
- /**
- * Contains all the fields of this group
- * @var array
- */
- protected $fields = array();
- /**
- * Contains the names of submitted values who don't belong to any fields
- * @var array
- */
- protected $extraFields = array();
- /**
- * Stores the class that the data of this form must be instances of
- * @var string
- */
- protected $dataClass;
- /**
- * The context used when creating the form
- * @var FormContext
- */
- protected $context = null;
- /**
- * Creates a new form with the options stored in the given context
- *
- * @param FormContextInterface $context
- * @param string $name
- * @param array $options
- * @return Form
- */
- public static function create(FormContextInterface $context, $name = null, array $options = array())
- {
- return new static($name, array_merge($context->getOptions(), $options));
- }
- /**
- * Constructor.
- *
- * @param string $name
- * @param array $options
- */
- public function __construct($name = null, array $options = array())
- {
- $this->addOption('data_class');
- $this->addOption('csrf_field_name', '_token');
- $this->addOption('csrf_provider');
- $this->addOption('field_factory');
- $this->addOption('validation_groups');
- $this->addOption('virtual', false);
- $this->addOption('validator');
- $this->addOption('context');
- if (isset($options['validation_groups'])) {
- $options['validation_groups'] = (array)$options['validation_groups'];
- }
- if (isset($options['data_class'])) {
- $this->dataClass = $options['data_class'];
- }
- parent::__construct($name, $options);
- // Enable CSRF protection
- if ($this->getOption('csrf_provider')) {
- if (!$this->getOption('csrf_provider') instanceof CsrfProviderInterface) {
- throw new FormException('The object passed to the "csrf_provider" option must implement CsrfProviderInterface');
- }
- $fieldName = $this->getOption('csrf_field_name');
- $token = $this->getOption('csrf_provider')->generateCsrfToken(get_class($this));
- $this->add(new HiddenField($fieldName, array('data' => $token)));
- }
- }
- /**
- * 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)
- {
- if ($this->isSubmitted()) {
- throw new AlreadySubmittedException('You cannot add fields after submitting a form');
- }
- // 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');
- }
- $factory = $this->getFieldFactory();
- if (!$factory) {
- throw new FormException('A field factory must be set to automatically create fields');
- }
- $class = $this->getDataClass();
- if (!$class) {
- throw new FormException('The data class must be set to automatically create fields');
- }
- $options = func_num_args() > 1 ? func_get_arg(1) : array();
- $field = $factory->getInstance($class, $field, $options);
- }
- 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)) {
- $field->readProperty($data);
- }
- return $field;
- }
- /**
- * 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;
- }
- /**
- * Returns an array of visible fields from the current schema.
- *
- * @return array
- */
- public function getVisibleFields()
- {
- return $this->getFieldsByVisibility(false, false);
- }
- /**
- * Returns an array of visible fields from the current schema.
- *
- * This variant of the method will recursively get all the
- * fields from the nested forms or field groups
- *
- * @return array
- */
- public function getAllVisibleFields()
- {
- return $this->getFieldsByVisibility(false, true);
- }
- /**
- * Returns an array of hidden fields from the current schema.
- *
- * @return array
- */
- public function getHiddenFields()
- {
- return $this->getFieldsByVisibility(true, false);
- }
- /**
- * Returns an array of hidden fields from the current schema.
- *
- * This variant of the method will recursively get all the
- * fields from the nested forms or field groups
- *
- * @return array
- */
- public function getAllHiddenFields()
- {
- return $this->getFieldsByVisibility(true, true);
- }
- /**
- * Returns a filtered array of fields from the current schema.
- *
- * @param Boolean $hidden Whether to return hidden fields only or visible fields only
- * @param Boolean $recursive Whether to recur through embedded schemas
- *
- * @return array
- */
- protected function getFieldsByVisibility($hidden, $recursive)
- {
- $fields = array();
- $hidden = (Boolean)$hidden;
- foreach ($this->fields as $field) {
- if ($field instanceof Form && $recursive) {
- $fields = array_merge($fields, $field->getFieldsByVisibility($hidden, $recursive));
- } else if ($hidden === $field->isHidden()) {
- $fields[] = $field;
- }
- }
- return $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)) {
- if ($this->dataClass && !$data instanceof $this->dataClass) {
- throw new FormException(sprintf('Form data should be instance of %s', $this->dataClass));
- }
- $this->readObject($data);
- }
- }
- /**
- * 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.
- *
- * @param string|array $data The POST data
- */
- public function submit($data)
- {
- if (null === $data) {
- $data = array();
- }
- if (!is_array($data)) {
- throw new UnexpectedTypeException($data, 'array');
- }
- // remember for later
- $submittedData = $data;
- foreach ($this->fields as $key => $field) {
- if (!isset($data[$key])) {
- $data[$key] = null;
- }
- }
- $data = $this->preprocessData($data);
- foreach ($data as $key => $value) {
- if ($this->has($key)) {
- $this->fields[$key]->submit($value);
- }
- }
- $data = $this->getTransformedData();
- $this->writeObject($data);
- // set and reverse transform the data
- parent::submit($data);
- $this->extraFields = array();
- foreach ($submittedData as $key => $value) {
- if (!$this->has($key)) {
- $this->extraFields[] = $key;
- }
- }
- }
- /**
- * Updates the child fields from the properties of the given data
- *
- * This method calls readProperty() on all child fields that have a
- * property path set. If a child field has no property path set but
- * implements FormInterface, writeProperty() is called on its
- * children instead.
- *
- * @param array|object $objectOrArray
- */
- protected function readObject(&$objectOrArray)
- {
- $iterator = new RecursiveFieldIterator($this);
- $iterator = new \RecursiveIteratorIterator($iterator);
- foreach ($iterator as $field) {
- $field->readProperty($objectOrArray);
- }
- }
- /**
- * Updates all properties of the given data from the child fields
- *
- * This method calls writeProperty() on all child fields that have a property
- * path set. If a child field has no property path set but implements
- * FormInterface, writeProperty() is called on its children instead.
- *
- * @param array|object $objectOrArray
- */
- protected function writeObject(&$objectOrArray)
- {
- $iterator = new RecursiveFieldIterator($this);
- $iterator = new \RecursiveIteratorIterator($iterator);
- foreach ($iterator as $field) {
- $field->writeProperty($objectOrArray);
- }
- }
- /**
- * Processes the submitted data before it is passed to the individual fields
- *
- * The data is in the user format.
- *
- * @param array $data
- * @return array
- */
- protected function preprocessData(array $data)
- {
- return $data;
- }
- /**
- * @inheritDoc
- */
- public function isVirtual()
- {
- return $this->getOption('virtual');
- }
- /**
- * Returns whether this form was submitted with extra fields
- *
- * @return Boolean
- */
- public function isSubmittedWithExtraFields()
- {
- // 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}
- */
- public function addError(FieldError $error, PropertyPathIterator $pathIterator = null, $type = null)
- {
- if (null !== $pathIterator) {
- if ($type === self::FIELD_ERROR && $pathIterator->hasNext()) {
- $pathIterator->next();
- if ($pathIterator->isProperty() && $pathIterator->current() === 'fields') {
- $pathIterator->next();
- }
- if ($this->has($pathIterator->current()) && !$this->get($pathIterator->current())->isHidden()) {
- $this->get($pathIterator->current())->addError($error, $pathIterator, $type);
- return;
- }
- } else if ($type === self::DATA_ERROR) {
- $iterator = new RecursiveFieldIterator($this);
- $iterator = new \RecursiveIteratorIterator($iterator);
- foreach ($iterator as $field) {
- if (null !== ($fieldPath = $field->getPropertyPath())) {
- if ($fieldPath->getElement(0) === $pathIterator->current() && !$field->isHidden()) {
- if ($pathIterator->hasNext()) {
- $pathIterator->next();
- }
- $field->addError($error, $pathIterator, $type);
- 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;
- }
- /**
- * Returns true if the field exists (implements the \ArrayAccess interface).
- *
- * @param string $key The key of the field
- *
- * @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);
- }
- /**
- * Returns a factory for automatically creating fields based on metadata
- * available for a form's object
- *
- * @return FieldFactoryInterface The factory
- */
- public function getFieldFactory()
- {
- return $this->getOption('field_factory');
- }
- /**
- * Returns the validator used by the form
- *
- * @return ValidatorInterface The validator instance
- */
- public function getValidator()
- {
- return $this->getOption('validator');
- }
- /**
- * Returns the validation groups validated by the form
- *
- * @return array A list of validation groups or null
- */
- public function getValidationGroups()
- {
- return $this->getOption('validation_groups');
- }
- /**
- * Returns the name used for the CSRF protection field
- *
- * @return string The field name
- */
- public function getCsrfFieldName()
- {
- return $this->getOption('csrf_field_name');
- }
- /**
- * Returns the provider used for generating and validating CSRF tokens
- *
- * @return CsrfProviderInterface The provider instance
- */
- public function getCsrfProvider()
- {
- return $this->getOption('csrf_provider');
- }
- /**
- * Binds a request to the form
- *
- * 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
- */
- public function bind(Request $request, $data = null)
- {
- // 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());
- $this->submit(self::deepArrayUnion($values, $files));
- $this->validate();
- }
- }
- /**
- * Validates the form and its domain object
- *
- * @throws FormException If the option "validator" was not set
- */
- public function validate()
- {
- if (null === $this->getOption('validator')) {
- throw new MissingOptionsException('The option "validator" is required for validating', array('validator'));
- }
- // Validate the submitted data
- if ($violations = $this->getOption('validator')->validate($this, $this->getOption('validation_groups'))) {
- // TODO: test me
- foreach ($violations as $violation) {
- $propertyPath = new PropertyPath($violation->getPropertyPath());
- $iterator = $propertyPath->getIterator();
- if ($iterator->current() == 'data') {
- $type = self::DATA_ERROR;
- $iterator->next(); // point at the first data element
- } else {
- $type = self::FIELD_ERROR;
- }
- $this->addError(new FieldError($violation->getMessageTemplate(), $violation->getMessageParameters()), $iterator, $type);
- }
- }
- }
- /**
- * @return true if this form is CSRF protected
- */
- public function isCsrfProtected()
- {
- return $this->has($this->getOption('csrf_field_name'));
- }
- /**
- * Returns whether the CSRF token is valid
- *
- * @return Boolean
- */
- public function isCsrfTokenValid()
- {
- if (!$this->isCsrfProtected()) {
- return true;
- } else {
- $token = $this->get($this->getOption('csrf_field_name'))->getDisplayedData();
- return $this->getOption('csrf_provider')->isCsrfTokenValid(get_class($this), $token);
- }
- }
- /**
- * Returns whether the maximum POST size was reached in this request.
- *
- * @return Boolean
- */
- public function isPostMaxSizeReached()
- {
- if ($this->isRoot() && isset($_SERVER['CONTENT_LENGTH'])) {
- $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;
- } else {
- return false;
- }
- }
- /**
- * Sets the class that object bound to this form must be instances of
- *
- * @param string A fully qualified class name
- */
- protected function setDataClass($class)
- {
- $this->dataClass = $class;
- }
- /**
- * 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;
- }
- /**
- * Returns the context used when creating this form
- *
- * @return FormContext The context instance
- */
- public function getContext()
- {
- return $this->getOption('context');
- }
- /**
- * 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;
- }
- }
|