diff --git a/UPGRADE-2.3.md b/UPGRADE-2.3.md
new file mode 100644
index 0000000000000..5e83e86ac24a5
--- /dev/null
+++ b/UPGRADE-2.3.md
@@ -0,0 +1,37 @@
+UPGRADE FROM 2.2 to 2.3
+=======================
+
+### Form
+
+ * Although this was not officially supported nor documented, it was possible to
+ set the option "validation_groups" to false, resulting in the group "Default"
+ being validated. Now, if you set "validation_groups" to false, the validation
+ of a form will be skipped (except for a few integrity checks on the form).
+
+ If you want to validate a form in group "Default", you should either
+ explicitly set "validation_groups" to "Default" or alternatively set it to
+ null.
+
+ Before:
+
+ ```
+ // equivalent notations for validating in group "Default"
+ "validation_groups" => null
+ "validation_groups" => "Default"
+ "validation_groups" => false
+
+ // notation for skipping validation
+ "validation_groups" => array()
+ ```
+
+ After:
+
+ ```
+ // equivalent notations for validating in group "Default"
+ "validation_groups" => null
+ "validation_groups" => "Default"
+
+ // equivalent notations for skipping validation
+ "validation_groups" => false
+ "validation_groups" => array()
+ ```
diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
index 3ab047c494ad8..3fe56557467c3 100644
--- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
+++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
@@ -218,6 +218,29 @@
{% endspaceless %}
{% endblock email_widget %}
+{% block button_widget %}
+{% spaceless %}
+ {% if label is empty %}
+ {% set label = name|humanize %}
+ {% endif %}
+
+{% endspaceless %}
+{% endblock button_widget %}
+
+{% block submit_widget %}
+{% spaceless %}
+ {% set type = type|default('submit') %}
+ {{ block('button_widget') }}
+{% endspaceless %}
+{% endblock submit_widget %}
+
+{% block reset_widget %}
+{% spaceless %}
+ {% set type = type|default('reset') %}
+ {{ block('button_widget') }}
+{% endspaceless %}
+{% endblock reset_widget %}
+
{# Labels #}
{% block form_label %}
@@ -237,6 +260,8 @@
{% endspaceless %}
{% endblock form_label %}
+{% block button_label %}{% endblock %}
+
{# Rows #}
{% block repeated_row %}
@@ -259,6 +284,14 @@
{% endspaceless %}
{% endblock form_row %}
+{% block button_row %}
+{% spaceless %}
+
+ {{ form_widget(form) }}
+
+{% endspaceless %}
+{% endblock button_row %}
+
{% block hidden_row %}
{{ form_widget(form) }}
{% endblock hidden_row %}
@@ -317,14 +350,9 @@
{% endspaceless %}
{% endblock widget_container_attributes %}
-{# Deprecated in Symfony 2.1, to be removed in 2.3 #}
-
-{% block generic_label %}{{ block('form_label') }}{% endblock %}
-{% block widget_choice_options %}{{ block('choice_widget_options') }}{% endblock %}
-{% block field_widget %}{{ block('form_widget_simple') }}{% endblock %}
-{% block field_label %}{{ block('form_label') }}{% endblock %}
-{% block field_row %}{{ block('form_row') }}{% endblock %}
-{% block field_enctype %}{{ block('form_enctype') }}{% endblock %}
-{% block field_errors %}{{ block('form_errors') }}{% endblock %}
-{% block field_rest %}{{ block('form_rest') }}{% endblock %}
-{% block field_rows %}{{ block('form_rows') }}{% endblock %}
+{% block button_attributes %}
+{% spaceless %}
+ id="{{ id }}" name="{{ full_name }}"{% if disabled %} disabled="disabled"{% endif %}
+ {% for attrname, attrvalue in attr %}{{ attrname }}="{{ attrvalue }}" {% endfor %}
+{% endspaceless %}
+{% endblock button_attributes %}
diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig
index 63bd7d2787137..1f339763c7258 100644
--- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig
+++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig
@@ -14,6 +14,17 @@
{% endspaceless %}
{% endblock form_row %}
+{% block button_row %}
+{% spaceless %}
+
+ |
+
+ {{ form_widget(form) }}
+ |
+
+{% endspaceless %}
+{% endblock button_row %}
+
{% block hidden_row %}
{% spaceless %}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml
index d614e4dc50757..17e459b9ea464 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml
@@ -140,6 +140,15 @@
+
+
+
+
+
+
+
+
+
@@ -154,5 +163,8 @@
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_attributes.html.php
new file mode 100644
index 0000000000000..63d16bd357dc6
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_attributes.html.php
@@ -0,0 +1,6 @@
+id="escape($id) ?>"
+name="escape($full_name) ?>"
+disabled="disabled"
+ $v): ?>
+ escape($k), $view->escape($v)) ?>
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_label.html.php
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_row.html.php
new file mode 100644
index 0000000000000..b52e92984533d
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_row.html.php
@@ -0,0 +1,3 @@
+
+ widget($form) ?>
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_widget.html.php
new file mode 100644
index 0000000000000..4f789273822ba
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_widget.html.php
@@ -0,0 +1,4 @@
+humanize($name); } ?>
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_enctype.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_enctype.html.php
deleted file mode 100644
index 2e107a7b26be0..0000000000000
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_enctype.html.php
+++ /dev/null
@@ -1 +0,0 @@
-block($form, 'form_enctype') ?>
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_errors.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_errors.html.php
deleted file mode 100644
index ffed7cf43cad0..0000000000000
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_errors.html.php
+++ /dev/null
@@ -1 +0,0 @@
-block($form, 'form_errors') ?>
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_label.html.php
deleted file mode 100644
index 0b59dfb301bc5..0000000000000
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_label.html.php
+++ /dev/null
@@ -1 +0,0 @@
-block($form, 'form_label') ?>
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_rest.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_rest.html.php
deleted file mode 100644
index ee1bc31333ed1..0000000000000
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_rest.html.php
+++ /dev/null
@@ -1 +0,0 @@
-block($form, 'form_rest') ?>
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_row.html.php
deleted file mode 100644
index 971d8ac5a9d76..0000000000000
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_row.html.php
+++ /dev/null
@@ -1 +0,0 @@
-block($form, 'form_row') ?>
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_rows.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_rows.html.php
deleted file mode 100644
index d4af23d712320..0000000000000
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_rows.html.php
+++ /dev/null
@@ -1 +0,0 @@
-block($form, 'form_rows') ?>
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_widget.html.php
deleted file mode 100644
index 91e5ef1e1c144..0000000000000
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_widget.html.php
+++ /dev/null
@@ -1 +0,0 @@
-block($form, 'form_widget_simple') ?>
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php
new file mode 100644
index 0000000000000..1575e8292801e
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php
@@ -0,0 +1 @@
+block($form, 'button_widget', array('type' => isset($type) ? $type : 'reset')) ?>
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/submit_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/submit_widget.html.php
new file mode 100644
index 0000000000000..d42bb2a78ffe9
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/submit_widget.html.php
@@ -0,0 +1 @@
+block($form, 'button_widget', array('type' => isset($type) ? $type : 'submit')) ?>
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/button_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/button_row.html.php
new file mode 100644
index 0000000000000..ef4d22b975081
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/button_row.html.php
@@ -0,0 +1,6 @@
+
+ |
+
+ widget($form) ?>
+ |
+
diff --git a/src/Symfony/Component/Config/Tests/Definition/ArrayNodeTest.php b/src/Symfony/Component/Config/Tests/Definition/ArrayNodeTest.php
index 6d241cad5725a..34a2aece5edb5 100644
--- a/src/Symfony/Component/Config/Tests/Definition/ArrayNodeTest.php
+++ b/src/Symfony/Component/Config/Tests/Definition/ArrayNodeTest.php
@@ -25,7 +25,7 @@ public function testNormalizeThrowsExceptionWhenFalseIsNotAllowed()
}
/**
- * @expectedException Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
+ * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
* @expectedExceptionMessage Unrecognized options "foo" under "root"
*/
public function testExceptionThrownOnUnrecognizedChild()
diff --git a/src/Symfony/Component/Form/Button.php b/src/Symfony/Component/Form/Button.php
new file mode 100644
index 0000000000000..23c47755300be
--- /dev/null
+++ b/src/Symfony/Component/Form/Button.php
@@ -0,0 +1,412 @@
+
+ *
+ * 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\Form\Exception\AlreadyBoundException;
+
+/**
+ * A form button.
+ *
+ * @author Bernhard Schussek
+ */
+class Button implements \IteratorAggregate, FormInterface
+{
+ /**
+ * @var FormInterface
+ */
+ private $parent;
+
+ /**
+ * @var FormConfigInterface
+ */
+ private $config;
+
+ /**
+ * @var Boolean
+ */
+ private $bound = false;
+
+ /**
+ * Creates a new button from a form configuration.
+ *
+ * @param FormConfigInterface $config The button's configuration.
+ */
+ public function __construct(FormConfigInterface $config)
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param mixed $offset
+ *
+ * @return Boolean Always returns false.
+ */
+ public function offsetExists($offset)
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param mixed $offset
+ *
+ * @throws \BadMethodCallException
+ */
+ public function offsetGet($offset)
+ {
+ throw new \BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param mixed $offset
+ * @param mixed $value
+ *
+ * @throws \BadMethodCallException
+ */
+ public function offsetSet($offset, $value)
+ {
+ throw new \BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param mixed $offset
+ *
+ * @throws \BadMethodCallException
+ */
+ public function offsetUnset($offset)
+ {
+ throw new \BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setParent(FormInterface $parent = null)
+ {
+ $this->parent = $parent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param int|string|FormInterface $child
+ * @param null $type
+ * @param array $options
+ *
+ * @throws \BadMethodCallException
+ */
+ public function add($child, $type = null, array $options = array())
+ {
+ throw new \BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $name
+ *
+ * @throws \BadMethodCallException
+ */
+ public function get($name)
+ {
+ throw new \BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param string $name
+ *
+ * @return Boolean Always returns false.
+ */
+ public function has($name)
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $name
+ *
+ * @throws \BadMethodCallException
+ */
+ public function remove($name)
+ {
+ throw new \BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all()
+ {
+ return array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getErrors()
+ {
+ return array();
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $modelData
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setData($modelData)
+ {
+ throw new \BadMethodCallException('Buttons cannot have data.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getData()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getNormData()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getViewData()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return array Always returns an empty array.
+ */
+ public function getExtraData()
+ {
+ return array();
+ }
+
+ /**
+ * Returns the button's configuration.
+ *
+ * @return FormConfigInterface The configuration.
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Returns whether the button is submitted.
+ *
+ * @return Boolean true if the button was submitted.
+ */
+ public function isBound()
+ {
+ return $this->bound;
+ }
+
+ /**
+ * Returns the name by which the button is identified in forms.
+ *
+ * @return string The name of the button.
+ */
+ public function getName()
+ {
+ return $this->config->getName();
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getPropertyPath()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param FormError $error
+ *
+ * @throws \BadMethodCallException
+ */
+ public function addError(FormError $error)
+ {
+ throw new \BadMethodCallException('Buttons cannot have errors.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns true.
+ */
+ public function isValid()
+ {
+ return true;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function isRequired()
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDisabled()
+ {
+ return $this->config->getDisabled();
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns true.
+ */
+ public function isEmpty()
+ {
+ return true;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns true.
+ */
+ public function isSynchronized()
+ {
+ return true;
+ }
+
+ /**
+ * Binds data to the button.
+ *
+ * @param null|string $submittedData The data
+ *
+ * @return Button The button instance
+ *
+ * @throws Exception\AlreadyBoundException If the form has already been bound.
+ */
+ public function bind($submittedData)
+ {
+ if ($this->bound) {
+ throw new AlreadyBoundException('A form can only be bound once');
+ }
+
+ $this->bound = true;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRoot()
+ {
+ return $this->parent ? $this->parent->getRoot() : $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRoot()
+ {
+ return null === $this->parent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createView(FormView $parent = null)
+ {
+ if (null === $parent && $this->parent) {
+ $parent = $this->parent->createView();
+ }
+
+ return $this->config->getType()->createView($this, $parent);
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return integer Always returns 0.
+ */
+ public function count()
+ {
+ return 0;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return \EmptyIterator Always returns an empty iterator.
+ */
+ public function getIterator()
+ {
+ return new \EmptyIterator();
+ }
+}
diff --git a/src/Symfony/Component/Form/ButtonBuilder.php b/src/Symfony/Component/Form/ButtonBuilder.php
new file mode 100644
index 0000000000000..9e61cbf403cfa
--- /dev/null
+++ b/src/Symfony/Component/Form/ButtonBuilder.php
@@ -0,0 +1,750 @@
+
+ *
+ * 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\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Form\Exception\FormException;
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+
+/**
+ * A builder for {@link Button} instances.
+ *
+ * @author Bernhard Schussek
+ */
+class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
+{
+ /**
+ * @var Boolean
+ */
+ protected $locked = false;
+
+ /**
+ * @var Boolean
+ */
+ private $disabled;
+
+ /**
+ * @var ResolvedFormTypeInterface
+ */
+ private $type;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var array
+ */
+ private $attributes = array();
+
+ /**
+ * @var array
+ */
+ private $options;
+
+ /**
+ * Creates a new button builder.
+ *
+ * @param string $name The name of the button.
+ * @param array $options The button's options.
+ *
+ * @throws FormException If the name is empty.
+ */
+ public function __construct($name, array $options)
+ {
+ if (empty($name) && 0 != $name) {
+ throw new InvalidArgumentException('Buttons cannot have empty names.');
+ }
+
+ $this->name = (string) $name;
+ $this->options = $options;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string|integer|FormBuilderInterface $child
+ * @param string|FormTypeInterface $type
+ * @param array $options
+ *
+ * @throws \BadMethodCallException
+ */
+ public function add($child, $type = null, array $options = array())
+ {
+ throw new \BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $name
+ * @param string|FormTypeInterface $type
+ * @param array $options
+ *
+ * @throws \BadMethodCallException
+ */
+ public function create($name, $type = null, array $options = array())
+ {
+ throw new \BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $name
+ *
+ * @throws \BadMethodCallException
+ */
+ public function get($name)
+ {
+ throw new \BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $name
+ *
+ * @throws \BadMethodCallException
+ */
+ public function remove($name)
+ {
+ throw new \BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param string $name
+ *
+ * @return Boolean Always returns false.
+ */
+ public function has($name)
+ {
+ return false;
+ }
+
+ /**
+ * Returns the children.
+ *
+ * @return array Always returns an empty array.
+ */
+ public function all()
+ {
+ return array();
+ }
+
+ /**
+ * Creates the button.
+ *
+ * @return Button The button
+ */
+ public function getForm()
+ {
+ return new Button($this->getFormConfig());
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $eventName
+ * @param callable $listener
+ * @param integer $priority
+ *
+ * @throws \BadMethodCallException
+ */
+ public function addEventListener($eventName, $listener, $priority = 0)
+ {
+ throw new \BadMethodCallException('Buttons do not support event listeners.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param EventSubscriberInterface $subscriber
+ *
+ * @throws \BadMethodCallException
+ */
+ public function addEventSubscriber(EventSubscriberInterface $subscriber)
+ {
+ throw new \BadMethodCallException('Buttons do not support event subscribers.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param DataTransformerInterface $viewTransformer
+ * @param Boolean $forcePrepend
+ *
+ * @throws \BadMethodCallException
+ */
+ public function addViewTransformer(DataTransformerInterface $viewTransformer, $forcePrepend = false)
+ {
+ throw new \BadMethodCallException('Buttons do not support data transformers.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @throws \BadMethodCallException
+ */
+ public function resetViewTransformers()
+ {
+ throw new \BadMethodCallException('Buttons do not support data transformers.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param DataTransformerInterface $modelTransformer
+ * @param Boolean $forceAppend
+ *
+ * @throws \BadMethodCallException
+ */
+ public function addModelTransformer(DataTransformerInterface $modelTransformer, $forceAppend = false)
+ {
+ throw new \BadMethodCallException('Buttons do not support data transformers.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @throws \BadMethodCallException
+ */
+ public function resetModelTransformers()
+ {
+ throw new \BadMethodCallException('Buttons do not support data transformers.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAttribute($name, $value)
+ {
+ $this->attributes[$name] = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAttributes(array $attributes)
+ {
+ $this->attributes = $attributes;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param DataMapperInterface $dataMapper
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setDataMapper(DataMapperInterface $dataMapper = null)
+ {
+ throw new \BadMethodCallException('Buttons do not support data mappers.');
+ }
+
+ /**
+ * Set whether the button is disabled.
+ *
+ * @param Boolean $disabled Whether the button is disabled
+ *
+ * @return ButtonBuilder The button builder.
+ */
+ public function setDisabled($disabled)
+ {
+ $this->disabled = $disabled;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param mixed $emptyData
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setEmptyData($emptyData)
+ {
+ throw new \BadMethodCallException('Buttons do not support empty data.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $errorBubbling
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setErrorBubbling($errorBubbling)
+ {
+ throw new \BadMethodCallException('Buttons do not support error bubbling.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $required
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setRequired($required)
+ {
+ throw new \BadMethodCallException('Buttons cannot be required.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param null $propertyPath
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setPropertyPath($propertyPath)
+ {
+ throw new \BadMethodCallException('Buttons do not support property paths.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $mapped
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setMapped($mapped)
+ {
+ throw new \BadMethodCallException('Buttons do not support data mapping.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $byReference
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setByReference($byReference)
+ {
+ throw new \BadMethodCallException('Buttons do not support data mapping.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $virtual
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setVirtual($virtual)
+ {
+ throw new \BadMethodCallException('Buttons cannot be virtual.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $compound
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setCompound($compound)
+ {
+ throw new \BadMethodCallException('Buttons cannot be compound.');
+ }
+
+ /**
+ * Sets the type of the button.
+ *
+ * @param ResolvedFormTypeInterface $type The type of the button.
+ *
+ * @return ButtonBuilder The button builder.
+ */
+ public function setType(ResolvedFormTypeInterface $type)
+ {
+ $this->type = $type;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param array $data
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setData($data)
+ {
+ throw new \BadMethodCallException('Buttons do not support data.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $locked
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setDataLocked($locked)
+ {
+ throw new \BadMethodCallException('Buttons do not support data locking.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param FormFactoryInterface $formFactory
+ *
+ * @return void
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setFormFactory(FormFactoryInterface $formFactory)
+ {
+ throw new \BadMethodCallException('Buttons do not support form factories.');
+ }
+
+ /**
+ * Builds and returns the button configuration.
+ *
+ * @return FormConfigInterface
+ */
+ public function getFormConfig()
+ {
+ // This method should be idempotent, so clone the builder
+ $config = clone $this;
+ $config->locked = true;
+
+ return $config;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getEventDispatcher()
+ {
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getPropertyPath()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getMapped()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getByReference()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getVirtual()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getCompound()
+ {
+ return false;
+ }
+
+ /**
+ * Returns the form type used to construct the button.
+ *
+ * @return ResolvedFormTypeInterface The button's type.
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return array Always returns an empty array.
+ */
+ public function getViewTransformers()
+ {
+ return array();
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return array Always returns an empty array.
+ */
+ public function getModelTransformers()
+ {
+ return array();
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getDataMapper()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getRequired()
+ {
+ return false;
+ }
+
+ /**
+ * Returns whether the button is disabled.
+ *
+ * @return Boolean Whether the button is disabled.
+ */
+ public function getDisabled()
+ {
+ return $this->disabled;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getErrorBubbling()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getEmptyData()
+ {
+ return null;
+ }
+
+ /**
+ * Returns additional attributes of the button.
+ *
+ * @return array An array of key-value combinations.
+ */
+ public function getAttributes()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * Returns whether the attribute with the given name exists.
+ *
+ * @param string $name The attribute name.
+ *
+ * @return Boolean Whether the attribute exists.
+ */
+ public function hasAttribute($name)
+ {
+ return array_key_exists($name, $this->attributes);
+ }
+
+ /**
+ * Returns the value of the given attribute.
+ *
+ * @param string $name The attribute name.
+ * @param mixed $default The value returned if the attribute does not exist.
+ *
+ * @return mixed The attribute value.
+ */
+ public function getAttribute($name, $default = null)
+ {
+ return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getData()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getDataClass()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getDataLocked()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getFormFactory()
+ {
+ return null;
+ }
+
+ /**
+ * Returns all options passed during the construction of the button.
+ *
+ * @return array The passed options.
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * Returns whether a specific option exists.
+ *
+ * @param string $name The option name,
+ *
+ * @return Boolean Whether the option exists.
+ */
+ public function hasOption($name)
+ {
+ return array_key_exists($name, $this->options);
+ }
+
+ /**
+ * Returns the value of a specific option.
+ *
+ * @param string $name The option name.
+ * @param mixed $default The value returned if the option does not exist.
+ *
+ * @return mixed The option value.
+ */
+ public function getOption($name, $default = null)
+ {
+ return array_key_exists($name, $this->options) ? $this->options[$name] : $default;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return integer Always returns 0.
+ */
+ public function count()
+ {
+ return 0;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return \EmptyIterator Always returns an empty iterator.
+ */
+ public function getIterator()
+ {
+ return new \EmptyIterator();
+ }
+}
diff --git a/src/Symfony/Component/Form/ButtonTypeInterface.php b/src/Symfony/Component/Form/ButtonTypeInterface.php
new file mode 100644
index 0000000000000..dd5117c4da777
--- /dev/null
+++ b/src/Symfony/Component/Form/ButtonTypeInterface.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form;
+
+/**
+ * A type that should be converted into a {@link Button} instance.
+ *
+ * @author Bernhard Schussek
+ */
+interface ButtonTypeInterface extends FormTypeInterface
+{
+}
diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md
index 08f1aec42f2be..b315af5cbc128 100644
--- a/src/Symfony/Component/Form/CHANGELOG.md
+++ b/src/Symfony/Component/Form/CHANGELOG.md
@@ -28,6 +28,8 @@ CHANGELOG
* added an optional PropertyAccessorInterface parameter to FormType,
ObjectChoiceList and PropertyPathMapper
* [BC BREAK] PropertyPathMapper and FormType now have a constructor
+ * [BC BREAK] setting the option "validation_groups" to ``false`` now disables validation
+ instead of assuming group "Default"
2.1.0
-----
diff --git a/src/Symfony/Component/Form/ClickableInterface.php b/src/Symfony/Component/Form/ClickableInterface.php
new file mode 100644
index 0000000000000..893f02df0aa70
--- /dev/null
+++ b/src/Symfony/Component/Form/ClickableInterface.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form;
+
+/**
+ * A clickable form element.
+ *
+ * @author Bernhard Schussek
+ */
+interface ClickableInterface
+{
+ /**
+ * Returns whether this element was clicked.
+ *
+ * @return Boolean Whether this element was clicked.
+ */
+ public function isClicked();
+}
diff --git a/src/Symfony/Component/Form/Exception/InvalidArgumentException.php b/src/Symfony/Component/Form/Exception/InvalidArgumentException.php
new file mode 100644
index 0000000000000..a270e0ce9e0bd
--- /dev/null
+++ b/src/Symfony/Component/Form/Exception/InvalidArgumentException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Exception;
+
+/**
+ * Base InvalidArgumentException for the Form component.
+ *
+ * @author Bernhard Schussek
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php
index ff7a1a3e0e688..3934d38261ba2 100644
--- a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php
+++ b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php
@@ -50,6 +50,9 @@ protected function loadTypes()
new Type\TimezoneType(),
new Type\UrlType(),
new Type\FileType(),
+ new Type\ButtonType(),
+ new Type\SubmitType(),
+ new Type\ResetType(),
);
}
}
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php b/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php
new file mode 100644
index 0000000000000..79333a6799982
--- /dev/null
+++ b/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php
@@ -0,0 +1,121 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * Encapsulates common logic of {@link FormType} and {@link ButtonType}.
+ *
+ * This type does not appear in the form's type inheritance chain and as such
+ * cannot be extended (via {@link FormTypeExtension}s) nor themed.
+ *
+ * @author Bernhard Schussek
+ */
+abstract class BaseType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->setDisabled($options['disabled']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $name = $form->getName();
+ $blockName = $options['block_name'] ?: $form->getName();
+ $translationDomain = $options['translation_domain'];
+
+ if ($view->parent) {
+ if ('' !== ($parentFullName = $view->parent->vars['full_name'])) {
+ $id = sprintf('%s_%s', $view->parent->vars['id'], $name);
+ $fullName = sprintf('%s[%s]', $parentFullName, $name);
+ $uniqueBlockPrefix = sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName);
+ } else {
+ $id = $name;
+ $fullName = $name;
+ $uniqueBlockPrefix = '_'.$blockName;
+ }
+
+ if (!$translationDomain) {
+ $translationDomain = $view->parent->vars['translation_domain'];
+ }
+ } else {
+ $id = $name;
+ $fullName = $name;
+ $uniqueBlockPrefix = '_'.$blockName;
+
+ // Strip leading underscores and digits. These are allowed in
+ // form names, but not in HTML4 ID attributes.
+ // http://www.w3.org/TR/html401/struct/global.html#adef-id
+ $id = ltrim($id, '_0123456789');
+ }
+
+ $blockPrefixes = array();
+ for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) {
+ array_unshift($blockPrefixes, $type->getName());
+ }
+ $blockPrefixes[] = $uniqueBlockPrefix;
+
+ if (!$translationDomain) {
+ $translationDomain = 'messages';
+ }
+
+ $view->vars = array_replace($view->vars, array(
+ 'form' => $view,
+ 'id' => $id,
+ 'name' => $name,
+ 'full_name' => $fullName,
+ 'disabled' => $form->isDisabled(),
+ 'label' => $options['label'],
+ 'multipart' => false,
+ 'attr' => $options['attr'],
+ 'block_prefixes' => $blockPrefixes,
+ 'unique_block_prefix' => $uniqueBlockPrefix,
+ 'translation_domain' => $translationDomain,
+ // Using the block name here speeds up performance in collection
+ // forms, where each entry has the same full block name.
+ // Including the type is important too, because if rows of a
+ // collection form have different types (dynamically), they should
+ // be rendered differently.
+ // https://github.com/symfony/symfony/issues/5038
+ 'cache_key' => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getName(),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'block_name' => null,
+ 'disabled' => false,
+ 'label' => null,
+ 'attr' => array(),
+ 'translation_domain' => null,
+ ));
+
+ $resolver->setAllowedTypes(array(
+ 'attr' => 'array',
+ ));
+ }
+}
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ButtonType.php b/src/Symfony/Component/Form/Extension/Core/Type/ButtonType.php
new file mode 100644
index 0000000000000..3569963bc4a9e
--- /dev/null
+++ b/src/Symfony/Component/Form/Extension/Core/Type/ButtonType.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\ButtonTypeInterface;
+
+/**
+ * A form button.
+ *
+ * @author Bernhard Schussek
+ */
+class ButtonType extends BaseType implements ButtonTypeInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'button';
+ }
+}
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php
index 16e7bbc7a0f3c..bd01f8f0407ae 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\Form\Extension\Core\Type;
-use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
@@ -23,7 +22,7 @@
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
-class FormType extends AbstractType
+class FormType extends BaseType
{
/**
* @var PropertyAccessorInterface
@@ -40,9 +39,10 @@ public function __construct(PropertyAccessorInterface $propertyAccessor = null)
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
+ parent::buildForm($builder, $options);
+
$builder
->setRequired($options['required'])
- ->setDisabled($options['disabled'])
->setErrorBubbling($options['error_bubbling'])
->setEmptyData($options['empty_data'])
->setPropertyPath($options['property_path'])
@@ -65,85 +65,34 @@ public function buildForm(FormBuilderInterface $builder, array $options)
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
+ parent::buildView($view, $form, $options);
+
$name = $form->getName();
- $blockName = $options['block_name'] ?: $form->getName();
$readOnly = $options['read_only'];
- $translationDomain = $options['translation_domain'];
if ($view->parent) {
if ('' === $name) {
throw new Exception('Form node with empty name can be used only as root form node.');
}
- if ('' !== ($parentFullName = $view->parent->vars['full_name'])) {
- $id = sprintf('%s_%s', $view->parent->vars['id'], $name);
- $fullName = sprintf('%s[%s]', $parentFullName, $name);
- $uniqueBlockPrefix = sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName);
- } else {
- $id = $name;
- $fullName = $name;
- $uniqueBlockPrefix = '_'.$blockName;
- }
-
// Complex fields are read-only if they themselves or their parents are.
if (!$readOnly) {
$readOnly = $view->parent->vars['read_only'];
}
-
- if (!$translationDomain) {
- $translationDomain = $view->parent->vars['translation_domain'];
- }
- } else {
- $id = $name;
- $fullName = $name;
- $uniqueBlockPrefix = '_'.$blockName;
-
- // Strip leading underscores and digits. These are allowed in
- // form names, but not in HTML4 ID attributes.
- // http://www.w3.org/TR/html401/struct/global.html#adef-id
- $id = ltrim($id, '_0123456789');
- }
-
- $blockPrefixes = array();
- for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) {
- array_unshift($blockPrefixes, $type->getName());
- }
- $blockPrefixes[] = $uniqueBlockPrefix;
-
- if (!$translationDomain) {
- $translationDomain = 'messages';
}
$view->vars = array_replace($view->vars, array(
- 'form' => $view,
- 'id' => $id,
- 'name' => $name,
- 'full_name' => $fullName,
- 'read_only' => $readOnly,
- 'errors' => $form->getErrors(),
- 'valid' => $form->isBound() ? $form->isValid() : true,
- 'value' => $form->getViewData(),
- 'data' => $form->getNormData(),
- 'disabled' => $form->isDisabled(),
- 'required' => $form->isRequired(),
- 'max_length' => $options['max_length'],
- 'pattern' => $options['pattern'],
- 'size' => null,
- 'label' => $options['label'],
- 'multipart' => false,
- 'attr' => $options['attr'],
- 'label_attr' => $options['label_attr'],
- 'compound' => $form->getConfig()->getCompound(),
- 'block_prefixes' => $blockPrefixes,
- 'unique_block_prefix' => $uniqueBlockPrefix,
- 'translation_domain' => $translationDomain,
- // Using the block name here speeds up performance in collection
- // forms, where each entry has the same full block name.
- // Including the type is important too, because if rows of a
- // collection form have different types (dynamically), they should
- // be rendered differently.
- // https://github.com/symfony/symfony/issues/5038
- 'cache_key' => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getName(),
+ 'read_only' => $readOnly,
+ 'errors' => $form->getErrors(),
+ 'valid' => $form->isBound() ? $form->isValid() : true,
+ 'value' => $form->getViewData(),
+ 'data' => $form->getNormData(),
+ 'required' => $form->isRequired(),
+ 'max_length' => $options['max_length'],
+ 'pattern' => $options['pattern'],
+ 'size' => null,
+ 'label_attr' => $options['label_attr'],
+ 'compound' => $form->getConfig()->getCompound(),
));
}
@@ -169,6 +118,8 @@ public function finishView(FormView $view, FormInterface $form, array $options)
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
+ parent::setDefaultOptions($resolver);
+
// Derive "data_class" option from passed "data" object
$dataClass = function (Options $options) {
return isset($options['data']) && is_object($options['data']) ? get_class($options['data']) : null;
@@ -202,29 +153,23 @@ public function setDefaultOptions(OptionsResolverInterface $resolver)
));
$resolver->setDefaults(array(
- 'block_name' => null,
'data_class' => $dataClass,
'empty_data' => $emptyData,
'trim' => true,
'required' => true,
'read_only' => false,
- 'disabled' => false,
'max_length' => null,
'pattern' => null,
'property_path' => null,
'mapped' => true,
'by_reference' => true,
'error_bubbling' => $errorBubbling,
- 'label' => null,
- 'attr' => array(),
'label_attr' => array(),
'virtual' => false,
'compound' => true,
- 'translation_domain' => null,
));
$resolver->setAllowedTypes(array(
- 'attr' => 'array',
'label_attr' => 'array',
));
}
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ResetType.php b/src/Symfony/Component/Form/Extension/Core/Type/ResetType.php
new file mode 100644
index 0000000000000..cf55f7c5910a1
--- /dev/null
+++ b/src/Symfony/Component/Form/Extension/Core/Type/ResetType.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\ButtonTypeInterface;
+
+/**
+ * A reset button.
+ *
+ * @author Bernhard Schussek
+ */
+class ResetType extends AbstractType implements ButtonTypeInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'button';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'reset';
+ }
+}
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/SubmitType.php b/src/Symfony/Component/Form/Extension/Core/Type/SubmitType.php
new file mode 100644
index 0000000000000..6d160b969214b
--- /dev/null
+++ b/src/Symfony/Component/Form/Extension/Core/Type/SubmitType.php
@@ -0,0 +1,46 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\SubmitButtonTypeInterface;
+
+/**
+ * A submit button.
+ *
+ * @author Bernhard Schussek
+ */
+class SubmitType extends AbstractType implements SubmitButtonTypeInterface
+{
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars['clicked'] = $form->isClicked();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'button';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'submit';
+ }
+}
diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php
index 167fcd95e9755..610329c030e7a 100644
--- a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php
+++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Form\Extension\Validator\Constraints;
+use Symfony\Component\Form\ClickableInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Extension\Validator\Util\ServerParams;
use Symfony\Component\Validator\Constraint;
@@ -171,15 +172,21 @@ private static function allowDataWalking(FormInterface $form)
*/
private static function getValidationGroups(FormInterface $form)
{
+ $button = self::findClickedButton($form->getRoot());
+
+ if (null !== $button) {
+ $groups = $button->getConfig()->getOption('validation_groups');
+
+ if (null !== $groups) {
+ return self::resolveValidationGroups($groups, $form);
+ }
+ }
+
do {
$groups = $form->getConfig()->getOption('validation_groups');
if (null !== $groups) {
- if (is_callable($groups)) {
- $groups = call_user_func($groups, $form);
- }
-
- return (array) $groups;
+ return self::resolveValidationGroups($groups, $form);
}
$form = $form->getParent();
@@ -187,4 +194,43 @@ private static function getValidationGroups(FormInterface $form)
return array(Constraint::DEFAULT_GROUP);
}
+
+ /**
+ * Extracts a clicked button from a form tree, if one exists.
+ *
+ * @param FormInterface $form The root form.
+ *
+ * @return ClickableInterface|null The clicked button or null.
+ */
+ private static function findClickedButton(FormInterface $form)
+ {
+ if ($form instanceof ClickableInterface && $form->isClicked()) {
+ return $form;
+ }
+
+ foreach ($form as $child) {
+ if (null !== ($button = self::findClickedButton($child))) {
+ return $button;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Post-processes the validation groups option for a given form.
+ *
+ * @param array|callable $groups The validation groups.
+ * @param FormInterface $form The validated form.
+ *
+ * @return array The validation groups.
+ */
+ private static function resolveValidationGroups($groups, FormInterface $form)
+ {
+ if (is_callable($groups)) {
+ $groups = call_user_func($groups, $form);
+ }
+
+ return (array) $groups;
+ }
}
diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/BaseValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/BaseValidatorExtension.php
new file mode 100644
index 0000000000000..7c5e6784ae7bc
--- /dev/null
+++ b/src/Symfony/Component/Form/Extension/Validator/Type/BaseValidatorExtension.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Extension\Validator\Type;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * Encapsulates common logic of {@link FormTypeValidatorExtension} and
+ * {@link SubmitTypeValidatorExtension}.
+ *
+ * @author Bernhard Schussek
+ */
+abstract class BaseValidatorExtension extends AbstractTypeExtension
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ // Make sure that validation groups end up as null, closure or array
+ $validationGroupsNormalizer = function (Options $options, $groups) {
+ if (false === $groups) {
+ return array();
+ }
+
+ if (empty($groups)) {
+ return null;
+ }
+
+ if (is_callable($groups)) {
+ return $groups;
+ }
+
+ return (array) $groups;
+ };
+
+ $resolver->setDefaults(array(
+ 'validation_groups' => null,
+ ));
+
+ $resolver->setNormalizers(array(
+ 'validation_groups' => $validationGroupsNormalizer,
+ ));
+ }
+}
diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php
index 6b9ac4e4f03ca..99ef51f2bfed5 100644
--- a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php
+++ b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php
@@ -22,7 +22,7 @@
/**
* @author Bernhard Schussek
*/
-class FormTypeValidatorExtension extends AbstractTypeExtension
+class FormTypeValidatorExtension extends BaseValidatorExtension
{
/**
* @var ValidatorInterface
@@ -53,18 +53,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
- // Make sure that validation groups end up as null, closure or array
- $validationGroupsNormalizer = function (Options $options, $groups) {
- if (empty($groups)) {
- return null;
- }
-
- if (is_callable($groups)) {
- return $groups;
- }
-
- return (array) $groups;
- };
+ parent::setDefaultOptions($resolver);
// Constraint should always be converted to an array
$constraintsNormalizer = function (Options $options, $constraints) {
@@ -73,8 +62,8 @@ public function setDefaultOptions(OptionsResolverInterface $resolver)
$resolver->setDefaults(array(
'error_mapping' => array(),
- 'validation_groups' => null,
- 'constraints' => null,
+ 'validation_constraint' => null,
+ 'constraints' => array(),
'cascade_validation' => false,
'invalid_message' => 'This value is not valid.',
'invalid_message_parameters' => array(),
@@ -83,7 +72,6 @@ public function setDefaultOptions(OptionsResolverInterface $resolver)
));
$resolver->setNormalizers(array(
- 'validation_groups' => $validationGroupsNormalizer,
'constraints' => $constraintsNormalizer,
));
}
diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/SubmitTypeValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/SubmitTypeValidatorExtension.php
new file mode 100644
index 0000000000000..5aad67fb3ad3d
--- /dev/null
+++ b/src/Symfony/Component/Form/Extension/Validator/Type/SubmitTypeValidatorExtension.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Extension\Validator\Type;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+
+/**
+ * @author Bernhard Schussek
+ */
+class SubmitTypeValidatorExtension extends AbstractTypeExtension
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getExtendedType()
+ {
+ return 'submit';
+ }
+}
diff --git a/src/Symfony/Component/Form/Extension/Validator/ValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/ValidatorExtension.php
index e6d6fea415a38..9cff22a276575 100644
--- a/src/Symfony/Component/Form/Extension/Validator/ValidatorExtension.php
+++ b/src/Symfony/Component/Form/Extension/Validator/ValidatorExtension.php
@@ -51,6 +51,7 @@ protected function loadTypeExtensions()
return array(
new Type\FormTypeValidatorExtension($this->validator),
new Type\RepeatedTypeValidatorExtension(),
+ new Type\SubmitTypeValidatorExtension(),
);
}
}
diff --git a/src/Symfony/Component/Form/FormBuilder.php b/src/Symfony/Component/Form/FormBuilder.php
index a626980c59a13..a778c9e09df8f 100644
--- a/src/Symfony/Component/Form/FormBuilder.php
+++ b/src/Symfony/Component/Form/FormBuilder.php
@@ -37,13 +37,6 @@ class FormBuilder extends FormConfigBuilder implements \IteratorAggregate, FormB
*/
private $unresolvedChildren = array();
- /**
- * The parent of this builder.
- *
- * @var FormBuilder
- */
- private $parent;
-
/**
* Creates a new form builder.
*
@@ -70,7 +63,6 @@ public function add($child, $type = null, array $options = array())
}
if ($child instanceof self) {
- $child->setParent($this);
$this->children[$child->getName()] = $child;
// In case an unresolved child with the same name exists
@@ -111,10 +103,10 @@ public function create($name, $type = null, array $options = array())
}
if (null !== $type) {
- return $this->getFormFactory()->createNamedBuilder($name, $type, null, $options, $this);
+ return $this->getFormFactory()->createNamedBuilder($name, $type, null, $options);
}
- return $this->getFormFactory()->createBuilderForProperty($this->getDataClass(), $name, null, $options, $this);
+ return $this->getFormFactory()->createBuilderForProperty($this->getDataClass(), $name, null, $options);
}
/**
@@ -149,9 +141,6 @@ public function remove($name)
unset($this->unresolvedChildren[$name]);
if (array_key_exists($name, $this->children)) {
- if ($this->children[$name] instanceof self) {
- $this->children[$name]->setParent(null);
- }
unset($this->children[$name]);
}
@@ -211,7 +200,6 @@ public function getFormConfig()
{
$config = parent::getFormConfig();
- $config->parent = null;
$config->children = array();
$config->unresolvedChildren = array();
@@ -238,44 +226,6 @@ public function getForm()
return $form;
}
- /**
- * {@inheritdoc}
- */
- public function getParent()
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
-
- return $this->parent;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setParent(FormBuilderInterface $parent = null)
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
-
- $this->parent = $parent;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function hasParent()
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
-
- return null !== $this->parent;
- }
-
/**
* {@inheritdoc}
*/
diff --git a/src/Symfony/Component/Form/FormConfigBuilder.php b/src/Symfony/Component/Form/FormConfigBuilder.php
index 7d297ed33d3cb..c52b4169e8a0d 100644
--- a/src/Symfony/Component/Form/FormConfigBuilder.php
+++ b/src/Symfony/Component/Form/FormConfigBuilder.php
@@ -392,7 +392,7 @@ public function getAttributes()
*/
public function hasAttribute($name)
{
- return isset($this->attributes[$name]);
+ return array_key_exists($name, $this->attributes);
}
/**
@@ -400,7 +400,7 @@ public function hasAttribute($name)
*/
public function getAttribute($name, $default = null)
{
- return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
+ return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
}
/**
@@ -448,7 +448,7 @@ public function getOptions()
*/
public function hasOption($name)
{
- return isset($this->options[$name]);
+ return array_key_exists($name, $this->options);
}
/**
@@ -456,7 +456,7 @@ public function hasOption($name)
*/
public function getOption($name, $default = null)
{
- return isset($this->options[$name]) ? $this->options[$name] : $default;
+ return array_key_exists($name, $this->options) ? $this->options[$name] : $default;
}
/**
diff --git a/src/Symfony/Component/Form/FormEvents.php b/src/Symfony/Component/Form/FormEvents.php
index 114bfcd226345..c46faf36abe36 100644
--- a/src/Symfony/Component/Form/FormEvents.php
+++ b/src/Symfony/Component/Form/FormEvents.php
@@ -1,5 +1,4 @@
createBuilder($type, $data, $options, $parent)->getForm();
+ return $this->createBuilder($type, $data, $options)->getForm();
}
/**
* {@inheritdoc}
*/
- public function createNamed($name, $type = 'form', $data = null, array $options = array(), FormBuilderInterface $parent = null)
+ public function createNamed($name, $type = 'form', $data = null, array $options = array())
{
- return $this->createNamedBuilder($name, $type, $data, $options, $parent)->getForm();
+ return $this->createNamedBuilder($name, $type, $data, $options)->getForm();
}
/**
* {@inheritdoc}
*/
- public function createForProperty($class, $property, $data = null, array $options = array(), FormBuilderInterface $parent = null)
+ public function createForProperty($class, $property, $data = null, array $options = array())
{
- return $this->createBuilderForProperty($class, $property, $data, $options, $parent)->getForm();
+ return $this->createBuilderForProperty($class, $property, $data, $options)->getForm();
}
/**
* {@inheritdoc}
*/
- public function createBuilder($type = 'form', $data = null, array $options = array(), FormBuilderInterface $parent = null)
+ public function createBuilder($type = 'form', $data = null, array $options = array())
{
$name = $type instanceof FormTypeInterface || $type instanceof ResolvedFormTypeInterface
? $type->getName()
: $type;
- return $this->createNamedBuilder($name, $type, $data, $options, $parent);
+ return $this->createNamedBuilder($name, $type, $data, $options);
}
/**
* {@inheritdoc}
*/
- public function createNamedBuilder($name, $type = 'form', $data = null, array $options = array(), FormBuilderInterface $parent = null)
+ public function createNamedBuilder($name, $type = 'form', $data = null, array $options = array())
{
if (null !== $data && !array_key_exists('data', $options)) {
$options['data'] = $data;
@@ -84,16 +84,16 @@ public function createNamedBuilder($name, $type = 'form', $data = null, array $o
throw new UnexpectedTypeException($type, 'string, Symfony\Component\Form\ResolvedFormTypeInterface or Symfony\Component\Form\FormTypeInterface');
}
- return $type->createBuilder($this, $name, $options, $parent);
+ return $type->createBuilder($this, $name, $options);
}
/**
* {@inheritdoc}
*/
- public function createBuilderForProperty($class, $property, $data = null, array $options = array(), FormBuilderInterface $parent = null)
+ public function createBuilderForProperty($class, $property, $data = null, array $options = array())
{
if (null === $guesser = $this->registry->getTypeGuesser()) {
- return $this->createNamedBuilder($property, 'text', $data, $options, $parent);
+ return $this->createNamedBuilder($property, 'text', $data, $options);
}
$typeGuess = $guesser->guessType($class, $property);
@@ -123,7 +123,7 @@ public function createBuilderForProperty($class, $property, $data = null, array
$options = array_merge($typeGuess->getOptions(), $options);
}
- return $this->createNamedBuilder($property, $type, $data, $options, $parent);
+ return $this->createNamedBuilder($property, $type, $data, $options);
}
/**
diff --git a/src/Symfony/Component/Form/FormFactoryInterface.php b/src/Symfony/Component/Form/FormFactoryInterface.php
index 98c53d9b530e0..8edfa95e521df 100644
--- a/src/Symfony/Component/Form/FormFactoryInterface.php
+++ b/src/Symfony/Component/Form/FormFactoryInterface.php
@@ -24,13 +24,12 @@ interface FormFactoryInterface
* @param string|FormTypeInterface $type The type of the form
* @param mixed $data The initial data
* @param array $options The options
- * @param FormBuilderInterface $parent The parent builder
*
* @return FormInterface The form named after the type
*
* @throws Exception\FormException if any given option is not applicable to the given type
*/
- public function create($type = 'form', $data = null, array $options = array(), FormBuilderInterface $parent = null);
+ public function create($type = 'form', $data = null, array $options = array());
/**
* Returns a form.
@@ -41,30 +40,28 @@ public function create($type = 'form', $data = null, array $options = array(), F
* @param string|FormTypeInterface $type The type of the form
* @param mixed $data The initial data
* @param array $options The options
- * @param FormBuilderInterface $parent The parent builder
*
* @return FormInterface The form
*
* @throws Exception\FormException if any given option is not applicable to the given type
*/
- public function createNamed($name, $type = 'form', $data = null, array $options = array(), FormBuilderInterface $parent = null);
+ public function createNamed($name, $type = 'form', $data = null, array $options = array());
/**
* Returns a form for a property of a class.
*
* @see createBuilderForProperty()
*
- * @param string $class The fully qualified class name
- * @param string $property The name of the property to guess for
- * @param mixed $data The initial data
- * @param array $options The options for the builder
- * @param FormBuilderInterface $parent The parent builder
+ * @param string $class The fully qualified class name
+ * @param string $property The name of the property to guess for
+ * @param mixed $data The initial data
+ * @param array $options The options for the builder
*
* @return FormInterface The form named after the property
*
* @throws Exception\FormException if any given option is not applicable to the form type
*/
- public function createForProperty($class, $property, $data = null, array $options = array(), FormBuilderInterface $parent = null);
+ public function createForProperty($class, $property, $data = null, array $options = array());
/**
* Returns a form builder.
@@ -72,13 +69,12 @@ public function createForProperty($class, $property, $data = null, array $option
* @param string|FormTypeInterface $type The type of the form
* @param mixed $data The initial data
* @param array $options The options
- * @param FormBuilderInterface $parent The parent builder
*
* @return FormBuilderInterface The form builder
*
* @throws Exception\FormException if any given option is not applicable to the given type
*/
- public function createBuilder($type = 'form', $data = null, array $options = array(), FormBuilderInterface $parent = null);
+ public function createBuilder($type = 'form', $data = null, array $options = array());
/**
* Returns a form builder.
@@ -87,13 +83,12 @@ public function createBuilder($type = 'form', $data = null, array $options = arr
* @param string|FormTypeInterface $type The type of the form
* @param mixed $data The initial data
* @param array $options The options
- * @param FormBuilderInterface $parent The parent builder
*
* @return FormBuilderInterface The form builder
*
* @throws Exception\FormException if any given option is not applicable to the given type
*/
- public function createNamedBuilder($name, $type = 'form', $data = null, array $options = array(), FormBuilderInterface $parent = null);
+ public function createNamedBuilder($name, $type = 'form', $data = null, array $options = array());
/**
* Returns a form builder for a property of a class.
@@ -101,15 +96,14 @@ public function createNamedBuilder($name, $type = 'form', $data = null, array $o
* If any of the 'max_length', 'required' and type options can be guessed,
* and are not provided in the options argument, the guessed value is used.
*
- * @param string $class The fully qualified class name
- * @param string $property The name of the property to guess for
- * @param mixed $data The initial data
- * @param array $options The options for the builder
- * @param FormBuilderInterface $parent The parent builder
+ * @param string $class The fully qualified class name
+ * @param string $property The name of the property to guess for
+ * @param mixed $data The initial data
+ * @param array $options The options for the builder
*
* @return FormBuilderInterface The form builder named after the property
*
* @throws Exception\FormException if any given option is not applicable to the form type
*/
- public function createBuilderForProperty($class, $property, $data = null, array $options = array(), FormBuilderInterface $parent = null);
+ public function createBuilderForProperty($class, $property, $data = null, array $options = array());
}
diff --git a/src/Symfony/Component/Form/FormInterface.php b/src/Symfony/Component/Form/FormInterface.php
index 94f6b3e29dc1a..3e8fe525ecc08 100644
--- a/src/Symfony/Component/Form/FormInterface.php
+++ b/src/Symfony/Component/Form/FormInterface.php
@@ -150,9 +150,9 @@ public function getExtraData();
public function getConfig();
/**
- * Returns whether the field is bound.
+ * Returns whether the form is submitted.
*
- * @return Boolean true if the form is bound to input values, false otherwise
+ * @return Boolean true if the form is submitted, false otherwise
*/
public function isBound();
diff --git a/src/Symfony/Component/Form/ResolvedFormType.php b/src/Symfony/Component/Form/ResolvedFormType.php
index f2ef06c8b8a49..e48ea5e64d277 100644
--- a/src/Symfony/Component/Form/ResolvedFormType.php
+++ b/src/Symfony/Component/Form/ResolvedFormType.php
@@ -104,16 +104,15 @@ public function getTypeExtensions()
/**
* {@inheritdoc}
*/
- public function createBuilder(FormFactoryInterface $factory, $name, array $options = array(), FormBuilderInterface $parent = null)
+ public function createBuilder(FormFactoryInterface $factory, $name, array $options = array())
{
$options = $this->getOptionsResolver()->resolve($options);
// Should be decoupled from the specific option at some point
$dataClass = isset($options['data_class']) ? $options['data_class'] : null;
- $builder = new FormBuilder($name, $dataClass, new EventDispatcher(), $factory, $options);
+ $builder = $this->newBuilder($name, $dataClass, $factory, $options);
$builder->setType($this);
- $builder->setParent($parent);
$this->buildForm($builder, $options);
@@ -127,7 +126,7 @@ public function createView(FormInterface $form, FormView $parent = null)
{
$options = $form->getConfig()->getOptions();
- $view = new FormView($parent);
+ $view = $this->newView($parent);
$this->buildView($view, $form, $options);
@@ -243,4 +242,43 @@ public function getOptionsResolver()
return $this->optionsResolver;
}
+
+ /**
+ * Creates a new builder instance.
+ *
+ * Override this method if you want to customize the builder class.
+ *
+ * @param string $name The name of the builder.
+ * @param string $dataClass The data class.
+ * @param FormFactoryInterface $factory The current form factory.
+ * @param array $options The builder options.
+ *
+ * @return FormBuilderInterface The new builder instance.
+ */
+ protected function newBuilder($name, $dataClass, FormFactoryInterface $factory, array $options)
+ {
+ if ($this->innerType instanceof ButtonTypeInterface) {
+ return new ButtonBuilder($name, $options);
+ }
+
+ if ($this->innerType instanceof SubmitButtonTypeInterface) {
+ return new SubmitButtonBuilder($name, $options);
+ }
+
+ return new FormBuilder($name, $dataClass, new EventDispatcher(), $factory, $options);
+ }
+
+ /**
+ * Creates a new view instance.
+ *
+ * Override this method if you want to customize the view class.
+ *
+ * @param FormView|null $parent The parent view, if available.
+ *
+ * @return FormView A new view instance.
+ */
+ protected function newView(FormView $parent = null)
+ {
+ return new FormView($parent);
+ }
}
diff --git a/src/Symfony/Component/Form/ResolvedFormTypeInterface.php b/src/Symfony/Component/Form/ResolvedFormTypeInterface.php
index 611bd3a25f81d..c999bcdc6d833 100644
--- a/src/Symfony/Component/Form/ResolvedFormTypeInterface.php
+++ b/src/Symfony/Component/Form/ResolvedFormTypeInterface.php
@@ -52,11 +52,10 @@ public function getTypeExtensions();
* @param FormFactoryInterface $factory The form factory.
* @param string $name The name for the builder.
* @param array $options The builder options.
- * @param FormBuilderInterface $parent The parent builder object or null.
*
* @return FormBuilderInterface The created form builder.
*/
- public function createBuilder(FormFactoryInterface $factory, $name, array $options = array(), FormBuilderInterface $parent = null);
+ public function createBuilder(FormFactoryInterface $factory, $name, array $options = array());
/**
* Creates a new form view for a form of this type.
diff --git a/src/Symfony/Component/Form/SubmitButton.php b/src/Symfony/Component/Form/SubmitButton.php
new file mode 100644
index 0000000000000..01af5c2e72128
--- /dev/null
+++ b/src/Symfony/Component/Form/SubmitButton.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form;
+
+/**
+ * A button that submits the form.
+ *
+ * @author Bernhard Schussek
+ */
+class SubmitButton extends Button implements ClickableInterface
+{
+ /**
+ * @var Boolean
+ */
+ private $clicked = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isClicked()
+ {
+ return $this->clicked;
+ }
+
+ /**
+ * Binds data to the button.
+ *
+ * @param null|string $submittedData The data
+ *
+ * @return SubmitButton The button instance
+ *
+ * @throws Exception\AlreadyBoundException If the form has already been bound.
+ */
+ public function bind($submittedData)
+ {
+ parent::bind($submittedData);
+
+ $this->clicked = null !== $submittedData;
+
+ return $this;
+ }
+}
diff --git a/src/Symfony/Component/Form/SubmitButtonBuilder.php b/src/Symfony/Component/Form/SubmitButtonBuilder.php
new file mode 100644
index 0000000000000..088fb74819a60
--- /dev/null
+++ b/src/Symfony/Component/Form/SubmitButtonBuilder.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form;
+
+/**
+ * A builder for {@link SubmitButton} instances.
+ *
+ * @author Bernhard Schussek
+ */
+class SubmitButtonBuilder extends ButtonBuilder
+{
+ /**
+ * Creates the button.
+ *
+ * @return Button The button
+ */
+ public function getForm()
+ {
+ return new SubmitButton($this->getFormConfig());
+ }
+}
diff --git a/src/Symfony/Component/Form/SubmitButtonTypeInterface.php b/src/Symfony/Component/Form/SubmitButtonTypeInterface.php
new file mode 100644
index 0000000000000..f7ac13f7ec92e
--- /dev/null
+++ b/src/Symfony/Component/Form/SubmitButtonTypeInterface.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form;
+
+/**
+ * A type that should be converted into a {@link SubmitButton} instance.
+ *
+ * @author Bernhard Schussek
+ */
+interface SubmitButtonTypeInterface extends FormTypeInterface
+{
+}
diff --git a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php
index 7fd743006e27c..391e81173ea0c 100644
--- a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php
+++ b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php
@@ -82,6 +82,22 @@ public function testRepeatedRow()
);
}
+ public function testButtonRow()
+ {
+ $form = $this->factory->createNamed('name', 'button');
+ $view = $form->createView();
+ $html = $this->renderRow($view);
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./button[@type="button"][@name="name"]
+ ]
+ [count(//label)=0]
+'
+ );
+ }
+
public function testRest()
{
$view = $this->factory->createNamedBuilder('name', 'form')
diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php
index bf2c29e8e1842..bad30f06798cc 100644
--- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php
+++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php
@@ -1762,4 +1762,38 @@ public function testEmptyRootFormName()
//input[@type="text"][@id="child"][@name="child"]'
, 2);
}
+
+ public function testButton()
+ {
+ $form = $this->factory->createNamed('name', 'button');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+ '/button[@type="button"][@name="name"]'
+ );
+ }
+
+ public function testButtonLabelIsEmpty()
+ {
+ $form = $this->factory->createNamed('name', 'button');
+
+ $this->assertSame('', $this->renderLabel($form->createView()));
+ }
+
+ public function testSubmit()
+ {
+ $form = $this->factory->createNamed('name', 'submit');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+ '/button[@type="submit"][@name="name"]'
+ );
+ }
+
+ public function testReset()
+ {
+ $form = $this->factory->createNamed('name', 'reset');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+ '/button[@type="reset"][@name="name"]'
+ );
+ }
}
diff --git a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php
index e0f62c4ab1a70..28fdba2dda167 100644
--- a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php
+++ b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php
@@ -125,6 +125,25 @@ public function testRepeatedRowWithErrors()
);
}
+ public function testButtonRow()
+ {
+ $form = $this->factory->createNamed('name', 'button');
+ $view = $form->createView();
+ $html = $this->renderRow($view);
+
+ $this->assertMatchesXpath($html,
+'/tr
+ [
+ ./td
+ [.=""]
+ /following-sibling::td
+ [./button[@type="button"][@name="name"]]
+ ]
+ [count(//label)=0]
+'
+ );
+ }
+
public function testRest()
{
$view = $this->factory->createNamedBuilder('name', 'form')
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTest.php
new file mode 100644
index 0000000000000..d56355c64490d
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/BaseTypeTest.php
@@ -0,0 +1,129 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Extension\Core\Type;
+
+/**
+ * @author Bernhard Schussek
+ */
+abstract class BaseTypeTest extends TypeTestCase
+{
+ public function testPassDisabledAsOption()
+ {
+ $form = $this->factory->create($this->getTestedType(), null, array('disabled' => true));
+
+ $this->assertTrue($form->isDisabled());
+ }
+
+ public function testPassIdAndNameToView()
+ {
+ $form = $this->factory->createNamed('name', $this->getTestedType());
+ $view = $form->createView();
+
+ $this->assertEquals('name', $view->vars['id']);
+ $this->assertEquals('name', $view->vars['name']);
+ $this->assertEquals('name', $view->vars['full_name']);
+ }
+
+ public function testStripLeadingUnderscoresAndDigitsFromId()
+ {
+ $form = $this->factory->createNamed('_09name', $this->getTestedType());
+ $view = $form->createView();
+
+ $this->assertEquals('name', $view->vars['id']);
+ $this->assertEquals('_09name', $view->vars['name']);
+ $this->assertEquals('_09name', $view->vars['full_name']);
+ }
+
+ public function testPassIdAndNameToViewWithParent()
+ {
+ $parent = $this->factory->createNamed('parent', 'form');
+ $parent->add($this->factory->createNamed('child', $this->getTestedType()));
+ $view = $parent->createView();
+
+ $this->assertEquals('parent_child', $view['child']->vars['id']);
+ $this->assertEquals('child', $view['child']->vars['name']);
+ $this->assertEquals('parent[child]', $view['child']->vars['full_name']);
+ }
+
+ public function testPassIdAndNameToViewWithGrandParent()
+ {
+ $parent = $this->factory->createNamed('parent', 'form');
+ $parent->add($this->factory->createNamed('child', 'form'));
+ $parent['child']->add($this->factory->createNamed('grand_child', $this->getTestedType()));
+ $view = $parent->createView();
+
+ $this->assertEquals('parent_child_grand_child', $view['child']['grand_child']->vars['id']);
+ $this->assertEquals('grand_child', $view['child']['grand_child']->vars['name']);
+ $this->assertEquals('parent[child][grand_child]', $view['child']['grand_child']->vars['full_name']);
+ }
+
+ public function testPassTranslationDomainToView()
+ {
+ $form = $this->factory->create($this->getTestedType(), null, array(
+ 'translation_domain' => 'domain',
+ ));
+ $view = $form->createView();
+
+ $this->assertSame('domain', $view->vars['translation_domain']);
+ }
+
+ public function testInheritTranslationDomainFromParent()
+ {
+ $parent = $this->factory->createNamed('parent', 'form', null, array(
+ 'translation_domain' => 'domain',
+ ));
+ $child = $this->factory->createNamed('child', $this->getTestedType());
+ $view = $parent->add($child)->createView();
+
+ $this->assertEquals('domain', $view['child']->vars['translation_domain']);
+ }
+
+ public function testPreferOwnTranslationDomain()
+ {
+ $parent = $this->factory->createNamed('parent', 'form', null, array(
+ 'translation_domain' => 'parent_domain',
+ ));
+ $child = $this->factory->createNamed('child', $this->getTestedType(), null, array(
+ 'translation_domain' => 'domain',
+ ));
+ $view = $parent->add($child)->createView();
+
+ $this->assertEquals('domain', $view['child']->vars['translation_domain']);
+ }
+
+ public function testDefaultTranslationDomain()
+ {
+ $parent = $this->factory->createNamed('parent', 'form');
+ $child = $this->factory->createNamed('child', $this->getTestedType());
+ $view = $parent->add($child)->createView();
+
+ $this->assertEquals('messages', $view['child']->vars['translation_domain']);
+ }
+
+ public function testPassLabelToView()
+ {
+ $form = $this->factory->createNamed('__test___field', $this->getTestedType(), null, array('label' => 'My label'));
+ $view = $form->createView();
+
+ $this->assertSame('My label', $view->vars['label']);
+ }
+
+ public function testPassMultipartFalseToView()
+ {
+ $form = $this->factory->create($this->getTestedType());
+ $view = $form->createView();
+
+ $this->assertFalse($view->vars['multipart']);
+ }
+
+ abstract protected function getTestedType();
+}
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ButtonTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ButtonTypeTest.php
new file mode 100644
index 0000000000000..55835e77feb73
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ButtonTypeTest.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Extension\Core\Type;
+
+/**
+ * @author Bernhard Schussek
+ */
+class ButtonTypeTest extends BaseTypeTest
+{
+ public function testCreateButtonInstances()
+ {
+ $this->assertInstanceOf('Symfony\Component\Form\Button', $this->factory->create('button'));
+ }
+
+ protected function getTestedType()
+ {
+ return 'button';
+ }
+}
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php
index 6b906ea4f34bc..f6663d6b65614 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php
@@ -50,8 +50,13 @@ public function setReferenceCopy($reference)
}
}
-class FormTypeTest extends TypeTestCase
+class FormTypeTest extends BaseTypeTest
{
+ public function testCreateFormInstances()
+ {
+ $this->assertInstanceOf('Symfony\Component\Form\Form', $this->factory->create('form'));
+ }
+
public function testPassRequiredAsOption()
{
$form = $this->factory->create('form', null, array('required' => false));
@@ -63,13 +68,6 @@ public function testPassRequiredAsOption()
$this->assertTrue($form->isRequired());
}
- public function testPassDisabledAsOption()
- {
- $form = $this->factory->create('form', null, array('disabled' => true));
-
- $this->assertTrue($form->isDisabled());
- }
-
public function testBoundDataIsTrimmedBeforeTransforming()
{
$form = $this->factory->createBuilder('form')
@@ -102,49 +100,6 @@ public function testBoundDataIsNotTrimmedBeforeTransformingIfNoTrimming()
$this->assertEquals('reverse[ a ]', $form->getData());
}
- public function testPassIdAndNameToView()
- {
- $form = $this->factory->createNamed('name', 'form');
- $view = $form->createView();
-
- $this->assertEquals('name', $view->vars['id']);
- $this->assertEquals('name', $view->vars['name']);
- $this->assertEquals('name', $view->vars['full_name']);
- }
-
- public function testStripLeadingUnderscoresAndDigitsFromId()
- {
- $form = $this->factory->createNamed('_09name', 'form');
- $view = $form->createView();
-
- $this->assertEquals('name', $view->vars['id']);
- $this->assertEquals('_09name', $view->vars['name']);
- $this->assertEquals('_09name', $view->vars['full_name']);
- }
-
- public function testPassIdAndNameToViewWithParent()
- {
- $parent = $this->factory->createNamed('parent', 'form');
- $parent->add($this->factory->createNamed('child', 'form'));
- $view = $parent->createView();
-
- $this->assertEquals('parent_child', $view['child']->vars['id']);
- $this->assertEquals('child', $view['child']->vars['name']);
- $this->assertEquals('parent[child]', $view['child']->vars['full_name']);
- }
-
- public function testPassIdAndNameToViewWithGrandParent()
- {
- $parent = $this->factory->createNamed('parent', 'form');
- $parent->add($this->factory->createNamed('child', 'form'));
- $parent['child']->add($this->factory->createNamed('grand_child', 'form'));
- $view = $parent->createView();
-
- $this->assertEquals('parent_child_grand_child', $view['child']['grand_child']->vars['id']);
- $this->assertEquals('grand_child', $view['child']['grand_child']->vars['name']);
- $this->assertEquals('parent[child][grand_child]', $view['child']['grand_child']->vars['full_name']);
- }
-
public function testNonReadOnlyFormWithReadOnlyParentBeingReadOnly()
{
$parent = $this->factory->createNamed('parent', 'form', null, array('read_only' => true));
@@ -180,57 +135,6 @@ public function testPassMaxLengthToView()
$this->assertSame(10, $view->vars['max_length']);
}
- public function testPassTranslationDomainToView()
- {
- $form = $this->factory->create('form', null, array('translation_domain' => 'test'));
- $view = $form->createView();
-
- $this->assertSame('test', $view->vars['translation_domain']);
- }
-
- public function testNonTranslationDomainFormWithTranslationDomainParentBeingTranslationDomain()
- {
- $parent = $this->factory->createNamed('parent', 'form', null, array('translation_domain' => 'test'));
- $child = $this->factory->createNamed('child', 'form');
- $view = $parent->add($child)->createView();
-
- $this->assertEquals('test', $view['child']->vars['translation_domain']);
- }
-
- public function testTranslationDomainFormWithNonTranslationDomainParentBeingTranslationDomain()
- {
- $parent = $this->factory->createNamed('parent', 'form');
- $child = $this->factory->createNamed('child', 'form', null, array('translation_domain' => 'test'));
- $view = $parent->add($child)->createView();
-
- $this->assertEquals('test', $view['child']->vars['translation_domain']);
- }
-
- public function testNonTranslationDomainFormWithNonTranslationDomainParentBeingTranslationDomainDefault()
- {
- $parent = $this->factory->createNamed('parent', 'form');
- $child = $this->factory->createNamed('child', 'form');
- $view = $parent->add($child)->createView();
-
- $this->assertEquals('messages', $view['child']->vars['translation_domain']);
- }
-
- public function testPassLabelToView()
- {
- $form = $this->factory->createNamed('__test___field', 'form', null, array('label' => 'My label'));
- $view = $form->createView();
-
- $this->assertSame('My label', $view->vars['label']);
- }
-
- public function testDefaultTranslationDomain()
- {
- $form = $this->factory->create('form');
- $view = $form->createView();
-
- $this->assertSame('messages', $view->vars['translation_domain']);
- }
-
public function testBindWithEmptyDataCreatesObjectIfClassAvailable()
{
$form = $this->factory->create('form', null, array(
@@ -404,6 +308,7 @@ public function testNameCanBeEmptyString()
$this->assertEquals('', $form->getName());
}
+
public function testSubformDoesntCallSetters()
{
$author = new FormTest_AuthorWithoutRefSetter(new Author());
@@ -523,14 +428,6 @@ function ($value) use ($ref2) { // reverseTransform
$this->assertSame($ref2, $author['referenceCopy']);
}
- public function testPassMultipartFalseToView()
- {
- $form = $this->factory->create('form');
- $view = $form->createView();
-
- $this->assertFalse($view->vars['multipart']);
- }
-
public function testPassMultipartTrueIfAnyChildIsMultipartToView()
{
$form = $this->factory->create('form');
@@ -661,4 +558,9 @@ public function testPassZeroLabelToView()
$this->assertSame('0', $view->vars['label']);
}
+
+ protected function getTestedType()
+ {
+ return 'form';
+ }
}
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/SubmitTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/SubmitTypeTest.php
new file mode 100644
index 0000000000000..b85d72f5680a4
--- /dev/null
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/SubmitTypeTest.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Extension\Core\Type;
+
+/**
+ * @author Bernhard Schussek
+ */
+class SubmitTypeTest extends TypeTestCase
+{
+ public function testCreateSubmitButtonInstances()
+ {
+ $this->assertInstanceOf('Symfony\Component\Form\SubmitButton', $this->factory->create('submit'));
+ }
+
+ public function testNotClickedByDefault()
+ {
+ $button = $this->factory->create('submit');
+
+ $this->assertFalse($button->isClicked());
+ }
+
+ public function testNotClickedIfBoundWithNull()
+ {
+ $button = $this->factory->create('submit');
+ $button->bind(null);
+
+ $this->assertFalse($button->isClicked());
+ }
+
+ public function testClickedIfBoundWithEmptyString()
+ {
+ $button = $this->factory->create('submit');
+ $button->bind('');
+
+ $this->assertTrue($button->isClicked());
+ }
+
+ public function testClickedIfBoundWithUnemptyString()
+ {
+ $button = $this->factory->create('submit');
+ $button->bind('foo');
+
+ $this->assertTrue($button->isClicked());
+ }
+}
diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php
index a3d9f26d98617..ce5035717d72c 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php
@@ -17,6 +17,7 @@
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
use Symfony\Component\Form\Extension\Validator\Constraints\FormValidator;
+use Symfony\Component\Form\SubmitButtonBuilder;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\NotBlank;
@@ -169,6 +170,51 @@ public function testValidateConstraintsEvenIfNoCascadeValidation()
$this->validator->validate($form, new Form());
}
+ public function testDontValidateIfNoValidationGroups()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $form = $this->getBuilder('name', '\stdClass', array(
+ 'validation_groups' => array(),
+ ))
+ ->setData($object)
+ ->getForm();
+
+ $form->setData($object);
+
+ $context->expects($this->never())
+ ->method('validate');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testDontValidateConstraintsIfNoValidationGroups()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+ $constraint1 = $this->getMock('Symfony\Component\Validator\Constraint');
+ $constraint2 = $this->getMock('Symfony\Component\Validator\Constraint');
+
+ $options = array(
+ 'validation_groups' => array(),
+ 'constraints' => array($constraint1, $constraint2),
+ );
+ $form = $this->getBuilder('name', '\stdClass', $options)
+ ->setData($object)
+ ->getForm();
+
+ // Launch transformer
+ $form->bind(array());
+
+ $context->expects($this->never())
+ ->method('validate');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
public function testDontValidateIfNotSynchronized()
{
$context = $this->getMockExecutionContext();
@@ -208,6 +254,46 @@ function () { throw new TransformationFailedException(); }
$this->validator->validate($form, new Form());
}
+ public function testAddInvalidErrorEvenIfNoValidationGroups()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $form = $this->getBuilder('name', '\stdClass', array(
+ 'invalid_message' => 'invalid_message_key',
+ // Invalid message parameters must be supported, because the
+ // invalid message can be a translation key
+ // see https://github.com/symfony/symfony/issues/5144
+ 'invalid_message_parameters' => array('{{ foo }}' => 'bar'),
+ 'validation_groups' => array(),
+ ))
+ ->setData($object)
+ ->addViewTransformer(new CallbackTransformer(
+ function ($data) { return $data; },
+ function () { throw new TransformationFailedException(); }
+ ))
+ ->getForm();
+
+ // Launch transformer
+ $form->bind('foo');
+
+ $context->expects($this->never())
+ ->method('validate');
+
+ $context->expects($this->once())
+ ->method('addViolation')
+ ->with(
+ 'invalid_message_key',
+ array('{{ value }}' => 'foo', '{{ foo }}' => 'bar'),
+ 'foo'
+ );
+ $context->expects($this->never())
+ ->method('addViolationAt');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
public function testDontValidateConstraintsIfNotSynchronized()
{
$context = $this->getMockExecutionContext();
@@ -313,6 +399,62 @@ public function testHandleClosureValidationGroups()
$this->validator->validate($form, new Form());
}
+ public function testUseValidationGroupOfClickedButton()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $parent = $this->getBuilder('parent', null, array('cascade_validation' => true))
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $form = $this->getForm('name', '\stdClass', array(
+ 'validation_groups' => 'form_group',
+ ));
+
+ $parent->add($form);
+ $parent->add($this->getClickedSubmitButton('submit', array(
+ 'validation_groups' => 'button_group',
+ )));
+
+ $form->setData($object);
+
+ $context->expects($this->once())
+ ->method('validate')
+ ->with($object, 'data', 'button_group', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testDontUseValidationGroupOfUnclickedButton()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $parent = $this->getBuilder('parent', null, array('cascade_validation' => true))
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $form = $this->getForm('name', '\stdClass', array(
+ 'validation_groups' => 'form_group',
+ ));
+
+ $parent->add($form);
+ $parent->add($this->getSubmitButton('submit', array(
+ 'validation_groups' => 'button_group',
+ )));
+
+ $form->setData($object);
+
+ $context->expects($this->once())
+ ->method('validate')
+ ->with($object, 'data', 'form_group', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
public function testUseInheritedValidationGroup()
{
$context = $this->getMockExecutionContext();
@@ -561,9 +703,21 @@ private function getBuilder($name = 'name', $dataClass = null, array $options =
return new FormBuilder($name, $dataClass, $this->dispatcher, $this->factory, $options);
}
- private function getForm($name = 'name', $dataClass = null)
+ private function getForm($name = 'name', $dataClass = null, array $options = array())
+ {
+ return $this->getBuilder($name, $dataClass, $options)->getForm();
+ }
+
+ private function getSubmitButton($name = 'name', array $options = array())
+ {
+ $builder = new SubmitButtonBuilder($name, $options);
+
+ return $builder->getForm();
+ }
+
+ private function getClickedSubmitButton($name = 'name', array $options = array())
{
- return $this->getBuilder($name, $dataClass)->getForm();
+ return $this->getSubmitButton($name, $options)->bind('');
}
/**
diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
index 13faefe95624a..094a26853b7c3 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
@@ -40,6 +40,15 @@ public function testValidationGroupsCanBeSetToArray()
$this->assertEquals(array('group1', 'group2'), $form->getConfig()->getOption('validation_groups'));
}
+ public function testValidationGroupsCanBeSetToFalse()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'validation_groups' => false,
+ ));
+
+ $this->assertEquals(array(), $form->getConfig()->getOption('validation_groups'));
+ }
+
public function testValidationGroupsCanBeSetToCallback()
{
$form = $this->factory->create('form', null, array(
diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php
index e8137df35cda3..83a5db6e1040b 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php
@@ -781,7 +781,7 @@ public function testDefaultErrorMapping($target, $childName, $childPath, $grandC
} else {
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName. ' should have an error, but has none');
+ $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
}
}
@@ -1253,7 +1253,7 @@ public function testCustomDataErrorMapping($target, $mapFrom, $mapTo, $childName
} else {
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName. ' should have an error, but has none');
+ $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
}
}
@@ -1442,7 +1442,7 @@ public function testCustomFormErrorMapping($target, $mapFrom, $mapTo, $errorName
$this->assertCount(0, $errorChild->getErrors(), $errorName.' should not have an error, but has one');
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName. ' should have an error, but has none');
+ $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
}
}
@@ -1497,7 +1497,7 @@ public function testVirtualFormErrorMapping($target, $childName, $childPath, $gr
} else {
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName. ' should have an error, but has none');
+ $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
}
}
}
diff --git a/src/Symfony/Component/Form/Tests/FormBuilderTest.php b/src/Symfony/Component/Form/Tests/FormBuilderTest.php
index 14c8746f8b9d2..4f8cf05da8ff7 100644
--- a/src/Symfony/Component/Form/Tests/FormBuilderTest.php
+++ b/src/Symfony/Component/Form/Tests/FormBuilderTest.php
@@ -200,62 +200,19 @@ public function testGetGuessedType()
$this->assertNotSame($builder, $this->builder);
}
- public function testGetParent()
- {
- $this->assertNull($this->builder->getParent());
- }
-
- public function testGetParentForAddedBuilder()
- {
- $builder = new FormBuilder('name', null, $this->dispatcher, $this->factory);
- $this->builder->add($builder);
- $this->assertSame($this->builder, $builder->getParent());
- }
-
- public function testGetParentForRemovedBuilder()
- {
- $builder = new FormBuilder('name', null, $this->dispatcher, $this->factory);
- $this->builder->add($builder);
- $this->builder->remove('name');
- $this->assertNull($builder->getParent());
- }
-
- public function testGetParentForCreatedBuilder()
- {
- $this->builder = new FormBuilder('name', 'stdClass', $this->dispatcher, $this->factory);
- $this->factory
- ->expects($this->once())
- ->method('createNamedBuilder')
- ->with('bar', 'text', null, array(), $this->builder)
- ;
-
- $this->factory
- ->expects($this->once())
- ->method('createBuilderForProperty')
- ->with('stdClass', 'foo', null, array(), $this->builder)
- ;
-
- $this->builder->create('foo');
- $this->builder->create('bar', 'text');
- }
-
public function testGetFormConfigErasesReferences()
{
$builder = new FormBuilder('name', null, $this->dispatcher, $this->factory);
- $builder->setParent(new FormBuilder('parent', null, $this->dispatcher, $this->factory));
$builder->add(new FormBuilder('child', null, $this->dispatcher, $this->factory));
$config = $builder->getFormConfig();
$reflClass = new \ReflectionClass($config);
- $parent = $reflClass->getProperty('parent');
$children = $reflClass->getProperty('children');
$unresolvedChildren = $reflClass->getProperty('unresolvedChildren');
- $parent->setAccessible(true);
$children->setAccessible(true);
$unresolvedChildren->setAccessible(true);
- $this->assertNull($parent->getValue($config));
$this->assertEmpty($children->getValue($config));
$this->assertEmpty($unresolvedChildren->getValue($config));
}
diff --git a/src/Symfony/Component/Form/Tests/FormFactoryTest.php b/src/Symfony/Component/Form/Tests/FormFactoryTest.php
index cbb72f2bb31d6..ea872b01edf28 100644
--- a/src/Symfony/Component/Form/Tests/FormFactoryTest.php
+++ b/src/Symfony/Component/Form/Tests/FormFactoryTest.php
@@ -171,25 +171,6 @@ public function testCreateNamedBuilderWithResolvedTypeInstance()
$this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $resolvedType, null, $options));
}
- public function testCreateNamedBuilderWithParentBuilder()
- {
- $options = array('a' => '1', 'b' => '2');
- $parentBuilder = $this->getMockFormBuilder();
- $resolvedType = $this->getMockResolvedType();
-
- $this->registry->expects($this->once())
- ->method('getType')
- ->with('type')
- ->will($this->returnValue($resolvedType));
-
- $resolvedType->expects($this->once())
- ->method('createBuilder')
- ->with($this->factory, 'name', $options, $parentBuilder)
- ->will($this->returnValue('BUILDER'));
-
- $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', null, $options, $parentBuilder));
- }
-
public function testCreateNamedBuilderFillsDataOption()
{
$givenOptions = array('a' => '1', 'b' => '2');
@@ -228,7 +209,7 @@ public function testCreateNamedBuilderDoesNotOverrideExistingDataOption()
}
/**
- * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
* @expectedExceptionMessage Expected argument of type "string, Symfony\Component\Form\ResolvedFormTypeInterface or Symfony\Component\Form\FormTypeInterface", "stdClass" given
*/
public function testCreateNamedBuilderThrowsUnderstandableException()
diff --git a/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php b/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php
index fd69dba171720..bb32a24122e58 100644
--- a/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php
+++ b/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php
@@ -134,10 +134,8 @@ public function testCreateBuilder()
->will($this->returnCallback($assertIndex(7)));
$factory = $this->getMockFormFactory();
- $parentBuilder = $this->getBuilder('parent');
- $builder = $resolvedType->createBuilder($factory, 'name', $givenOptions, $parentBuilder);
+ $builder = $resolvedType->createBuilder($factory, 'name', $givenOptions);
- $this->assertSame($parentBuilder, $builder->getParent());
$this->assertSame($resolvedType, $builder->getType());
}
diff --git a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php
index c7fbab2d9c67b..a420a95f8c31e 100644
--- a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php
+++ b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php
@@ -89,7 +89,7 @@ public function testLoadThrowsAnExceptionIfFileNotLocal()
}
/**
- * @expectedException Symfony\Component\Translation\Exception\InvalidResourceException
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
* @expectedExceptionMessage Document types are not allowed.
*/
public function testDocTypeIsNotAllowed()
diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php
index 22478e606d0ec..1677b81b1f62c 100644
--- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php
+++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php
@@ -82,7 +82,7 @@ public function testLoadGroupSequenceProvider()
}
/**
- * @expectedException Symfony\Component\Validator\Exception\MappingException
+ * @expectedException \Symfony\Component\Validator\Exception\MappingException
* @expectedExceptionMessage Document types are not allowed.
*/
public function testDocTypeIsNotAllowed()