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 3ab047c494..4f3856914b 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,36 @@
{% endspaceless %}
{% endblock email_widget %}
+{% block button_widget %}
+{% spaceless %}
+ {% if type is not defined or type is empty %}
+ {% set type = "button" %}
+ {% endif %}
+ {% if label is empty %}
+ {% set label = name|humanize %}
+ {% endif %}
+
+{% endspaceless %}
+{% endblock button_widget %}
+
+{% block submit_widget %}
+{% spaceless %}
+ {% if type is not defined %}
+ {% set type = "submit" %}
+ {% endif %}
+ {{ block('button_widget') }}
+{% endspaceless %}
+{% endblock submit_widget %}
+
+{% block reset_widget %}
+{% spaceless %}
+ {% if type is not defined %}
+ {% set type = "reset" %}
+ {% endif %}
+ {{ block('button_widget') }}
+{% endspaceless %}
+{% endblock reset_widget %}
+
{# Labels #}
{% block form_label %}
@@ -237,6 +267,8 @@
{% endspaceless %}
{% endblock form_label %}
+{% block button_label %}{% endblock %}
+
{# Rows #}
{% block repeated_row %}
@@ -259,6 +291,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,6 +357,13 @@
{% endspaceless %}
{% endblock widget_container_attributes %}
+{% 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 %}
+
{# Deprecated in Symfony 2.1, to be removed in 2.3 #}
{% block generic_label %}{{ block('form_label') }}{% endblock %}
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 63bd7d2787..1f339763c7 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 d614e4dc50..a70e06becb 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml
@@ -140,6 +140,15 @@
+
+
+
+
+
+
+
+
+
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 0000000000..63d16bd357
--- /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 0000000000..e69de29bb2
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 0000000000..b52e929845
--- /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 0000000000..4f78927382
--- /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/reset_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php
new file mode 100644
index 0000000000..1575e82928
--- /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 0000000000..d42bb2a78f
--- /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 0000000000..ef4d22b975
--- /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/Form/Button.php b/src/Symfony/Component/Form/Button.php
new file mode 100644
index 0000000000..b1b0b00d65
--- /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;
+
+/**
+ * Default implementation of {@link ButtonInterface}.
+ *
+ * @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 0000000000..ce30b0ab26
--- /dev/null
+++ b/src/Symfony/Component/Form/ButtonBuilder.php
@@ -0,0 +1,823 @@
+
+ *
+ * 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;
+
+/**
+ * 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 FormException('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.
+ *
+ * Does nothing.
+ *
+ * @param FormBuilderInterface $parent The parent builder
+ *
+ * @deprecated Deprecated since version 2.2, to be removed in 2.3. You
+ * should not rely on the parent of a builder, because it is
+ * likely that the parent is only set after turning the builder
+ * into a form.
+ */
+ public function setParent(FormBuilderInterface $parent = null)
+ {
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ *
+ * @deprecated Deprecated since version 2.2, to be removed in 2.3. You
+ * should not rely on the parent of a builder, because it is
+ * likely that the parent is only set after turning the builder
+ * into a form.
+ */
+ public function getParent()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ *
+ * @deprecated Deprecated since version 2.2, to be removed in 2.3. You
+ * should not rely on the parent of a builder, because it is
+ * likely that the parent is only set after turning the builder
+ * into a form.
+ */
+ public function hasParent()
+ {
+ return false;
+ }
+
+ /**
+ * 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 FormValidatorInterface $validator
+ *
+ * @throws \BadMethodCallException
+ *
+ * @deprecated Deprecated since version 2.1, to be removed in 2.3.
+ */
+ public function addValidator(FormValidatorInterface $validator)
+ {
+ throw new \BadMethodCallException('Buttons do not support validators.');
+ }
+
+ /**
+ * 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 array Always returns null.
+ *
+ * @deprecated Deprecated since version 2.1, to be removed in 2.3.
+ */
+ public function getValidators()
+ {
+ return array();
+ }
+
+ /**
+ * 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 isset($this->attributes[$name]);
+ }
+
+ /**
+ * 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 isset($this->attributes[$name]) ? $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 isset($this->options[$name]);
+ }
+
+ /**
+ * 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 isset($this->options[$name]) ? $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 0000000000..dd5117c4da
--- /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/ClickableInterface.php b/src/Symfony/Component/Form/ClickableInterface.php
new file mode 100644
index 0000000000..893f02df0a
--- /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/Extension/Core/CoreExtension.php b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php
index ff7a1a3e0e..3934d38261 100644
--- a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php
+++ b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php
@@ -50,6 +50,9 @@ class CoreExtension extends AbstractExtension
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 0000000000..ca47a7bc2f
--- /dev/null
+++ b/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php
@@ -0,0 +1,123 @@
+
+ *
+ * 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)
+ {
+ /* @var \Symfony\Component\Form\ClickableInterface $form */
+
+ $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 0000000000..3569963bc4
--- /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 16e7bbc7a0..bd01f8f040 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\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
-class FormType extends AbstractType
+class FormType extends BaseType
{
/**
* @var PropertyAccessorInterface
@@ -40,9 +39,10 @@ class FormType extends AbstractType
*/
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 @@ class FormType extends AbstractType
*/
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 @@ class FormType extends AbstractType
*/
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 @@ class FormType extends AbstractType
));
$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 0000000000..cf55f7c591
--- /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 0000000000..6d160b9692
--- /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/Type/FormTypeValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php
index 6b9ac4e4f0..19c888e200 100644
--- a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php
+++ b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php
@@ -53,17 +53,9 @@ class FormTypeValidatorExtension extends AbstractTypeExtension
*/
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;
+ // BC clause
+ $constraints = function (Options $options) {
+ return $options['validation_constraint'];
};
// Constraint should always be converted to an array
@@ -73,8 +65,9 @@ class FormTypeValidatorExtension extends AbstractTypeExtension
$resolver->setDefaults(array(
'error_mapping' => array(),
- 'validation_groups' => null,
- 'constraints' => null,
+ // "validation_constraint" is deprecated. Use "constraints".
+ 'validation_constraint' => null,
+ 'constraints' => $constraints,
'cascade_validation' => false,
'invalid_message' => 'This value is not valid.',
'invalid_message_parameters' => array(),
@@ -83,7 +76,6 @@ class FormTypeValidatorExtension extends AbstractTypeExtension
));
$resolver->setNormalizers(array(
- 'validation_groups' => $validationGroupsNormalizer,
'constraints' => $constraintsNormalizer,
));
}
diff --git a/src/Symfony/Component/Form/FormInterface.php b/src/Symfony/Component/Form/FormInterface.php
index 94f6b3e29d..3e8fe525ec 100644
--- a/src/Symfony/Component/Form/FormInterface.php
+++ b/src/Symfony/Component/Form/FormInterface.php
@@ -150,9 +150,9 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
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 f2ef06c8b8..7203817b94 100644
--- a/src/Symfony/Component/Form/ResolvedFormType.php
+++ b/src/Symfony/Component/Form/ResolvedFormType.php
@@ -111,7 +111,7 @@ class ResolvedFormType implements ResolvedFormTypeInterface
// 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);
@@ -127,7 +127,7 @@ class ResolvedFormType implements ResolvedFormTypeInterface
{
$options = $form->getConfig()->getOptions();
- $view = new FormView($parent);
+ $view = $this->newView($parent);
$this->buildView($view, $form, $options);
@@ -243,4 +243,43 @@ class ResolvedFormType implements ResolvedFormTypeInterface
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/SubmitButton.php b/src/Symfony/Component/Form/SubmitButton.php
new file mode 100644
index 0000000000..01af5c2e72
--- /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 0000000000..088fb74819
--- /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 0000000000..f7ac13f7ec
--- /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 7fd743006e..391e81173e 100644
--- a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php
+++ b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php
@@ -82,6 +82,22 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
);
}
+ 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 bf2c29e8e1..bad30f0679 100644
--- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php
+++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php
@@ -1762,4 +1762,38 @@ abstract class AbstractLayoutTest extends FormIntegrationTestCase
//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 e0f62c4ab1..28fdba2dda 100644
--- a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php
+++ b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php
@@ -125,6 +125,25 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
);
}
+ 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 0000000000..d56355c644
--- /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 0000000000..55835e77fe
--- /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 6b906ea4f3..f6663d6b65 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 @@ class FormTest_AuthorWithoutRefSetter
}
}
-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 @@ class FormTypeTest extends TypeTestCase
$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 @@ class FormTypeTest extends TypeTestCase
$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 @@ class FormTypeTest extends TypeTestCase
$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 @@ class FormTypeTest extends TypeTestCase
$this->assertEquals('', $form->getName());
}
+
public function testSubformDoesntCallSetters()
{
$author = new FormTest_AuthorWithoutRefSetter(new Author());
@@ -523,14 +428,6 @@ class FormTypeTest extends TypeTestCase
$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 @@ class FormTypeTest extends TypeTestCase
$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 0000000000..b85d72f568
--- /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());
+ }
+}