merged branch bschussek/issue5493 (PR #6522)

This PR was merged into the master branch.

Discussion
----------

[2.3] [Form] Implemented form processors

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes
Fixes the following tickets: partially #5493
Todo: -
License of the code: MIT
Documentation PR: symfony/symfony-docs#2092

Commits
-------

11fee06 [TwigBridge] Removed duplicate entries from the CHANGELOG
68f360c [Form] Moved upgrade nodes to UPGRADE-3.0
01b71a4 [Form] Removed trigger_error() for deprecations as of 3.0
81f8c67 [Form] Implemented form processors
0ea75db [Form] Improved FormRenderer::renderBlock() to be usable outside of form blocks
This commit is contained in:
Fabien Potencier 2013-04-19 08:01:34 +02:00
commit fcd941c033
42 changed files with 1968 additions and 55 deletions

View File

@ -1,4 +1,4 @@
UPGRADE FROM 2.2 to 2.3
UPGRADE FROM 2.2 to 2.3
=======================
### Form

View File

@ -18,6 +18,97 @@ UPGRADE FROM 2.x to 3.0
`DebugClassLoader`. The difference is that the constructor now takes a
loader to wrap.
### Form
* Passing a `Symfony\Component\HttpFoundation\Request` instance to
`FormInterface::bind()` was disabled. You should use
`FormInterface::process()` instead.
Before:
```
if ('POST' === $request->getMethod()) {
$form->bind($request);
if ($form->isValid()) {
// ...
}
}
```
After:
```
if ($form->process($request)->isValid()) {
// ...
}
```
If you want to test whether the form was submitted separately, you can use
the method `isBound()`:
```
if ($form->process($request)->isBound()) {
// ...
if ($form->isValid()) {
// ...
}
}
```
### FrameworkBundle
* The `enctype` method of the `form` helper was removed. You should use the
new method `start` instead.
Before:
```
<form method="post" action="http://example.com" <?php echo $view['form']->enctype($form) ?>>
...
</form>
```
After:
```
<?php echo $view['form']->start($form) ?>
...
<?php echo $view['form']->end($form) ?>
```
The method and action of the form default to "POST" and the current
document. If you want to change these values, you can set them explicitly in
the controller.
Alternative 1:
```
$form = $this->createForm('my_form', $formData, array(
'method' => 'PUT',
'action' => $this->generateUrl('target_route'),
));
```
Alternative 2:
```
$form = $this->createFormBuilder($formData)
// ...
->setMethod('PUT')
->setAction($this->generateUrl('target_route'))
->getForm();
```
It is also possible to override the method and the action in the template:
```
<?php echo $view['form']->start($form, array('method' => 'GET', 'action' => 'http://example.com')) ?>
...
<?php echo $view['form']->end($form) ?>
```
### HttpKernel
* The `Symfony\Component\HttpKernel\Log\LoggerInterface` has been removed in
@ -98,6 +189,56 @@ UPGRADE FROM 2.x to 3.0
* The `render` tag is deprecated in favor of the `render` function.
* The `form_enctype` helper was removed. You should use the new `form_start`
function instead.
Before:
```
<form method="post" action="http://example.com" {{ form_enctype(form) }}>
...
</form>
```
After:
```
{{ form_start(form) }}
...
{{ form_end(form) }}
```
The method and action of the form default to "POST" and the current
document. If you want to change these values, you can set them explicitly in
the controller.
Alternative 1:
```
$form = $this->createForm('my_form', $formData, array(
'method' => 'PUT',
'action' => $this->generateUrl('target_route'),
));
```
Alternative 2:
```
$form = $this->createFormBuilder($formData)
// ...
->setMethod('PUT')
->setAction($this->generateUrl('target_route'))
->getForm();
```
It is also possible to override the method and the action in the template:
```
{{ form_start(form, {'method': 'GET', 'action': 'http://example.com'}) }}
...
{{ form_end(form) }}
```
### Yaml
* The ability to pass file names to `Yaml::parse()` has been removed.

View File

@ -1,6 +1,12 @@
CHANGELOG
=========
2.3.0
-----
* added helpers form(), form_start() and form_end()
* deprecated form_enctype() in favor of form_start()
2.2.0
-----

View File

@ -61,12 +61,14 @@ class FormExtension extends \Twig_Extension
public function getFunctions()
{
return array(
'form_enctype' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
'form_enctype' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\FormEnctypeNode', array('is_safe' => array('html'))),
'form_widget' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
'form_errors' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
'form_label' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
'form_row' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
'form_rest' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
'form_start' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\RenderBlockNode', array('is_safe' => array('html'))),
'form_end' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\RenderBlockNode', array('is_safe' => array('html'))),
'csrf_token' => new \Twig_Function_Method($this, 'renderer->renderCsrfToken'),
);
}

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Node;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
* the helper "form_start()" instead.
*/
class FormEnctypeNode extends SearchAndRenderBlockNode
{
public function compile(\Twig_Compiler $compiler)
{
parent::compile($compiler);
$compiler->raw(";\n");
// Uncomment this as soon as the deprecation note should be shown
// $compiler->write('trigger_error(\'The helper form_enctype(form) is deprecated since version 2.3 and will be removed in 3.0. Use form_start(form) instead.\', E_USER_DEPRECATED)');
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Node;
/**
* Compiles a call to {@link FormRendererInterface::renderBlock()}.
*
* The function name is used as block name. For example, if the function name
* is "foo", the block "foo" will be rendered.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RenderBlockNode extends \Twig_Node_Expression_Function
{
public function compile(\Twig_Compiler $compiler)
{
$compiler->addDebugInfo($this);
$arguments = iterator_to_array($this->getNode('arguments'));
$compiler->write('$this->env->getExtension(\'form\')->renderer->renderBlock(');
if (isset($arguments[0])) {
$compiler->subcompile($arguments[0]);
$compiler->raw(', \'' . $this->getAttribute('name') . '\'');
if (isset($arguments[1])) {
$compiler->raw(', ');
$compiler->subcompile($arguments[1]);
}
}
$compiler->raw(')');
}
}

View File

@ -298,6 +298,38 @@
{# Misc #}
{% block form %}
{% spaceless %}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
{% endspaceless %}
{% endblock form %}
{% block form_start %}
{% spaceless %}
{% set method = method|upper %}
{% if method in ["GET", "POST"] %}
{% set form_method = method %}
{% else %}
{% set form_method = "POST" %}
{% endif %}
<form method="{{ form_method|lower }}" action="{{ action }}"{% for attrname, attrvalue in attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}{% if multipart %} enctype="multipart/form-data"{% endif %}>
{% if form_method != method %}
<input type="hidden" name="_method" value="{{ method }}" />
{% endif %}
{% endspaceless %}
{% endblock form_start %}
{% block form_end %}
{% spaceless %}
{% if not render_rest is defined or render_rest %}
{{ form_rest(form) }}
{% endif %}
</form>
{% endspaceless %}
{% endblock form_end %}
{% block form_enctype %}
{% spaceless %}
{% if multipart %}enctype="multipart/form-data"{% endif %}

View File

@ -139,6 +139,11 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
$this->assertSame($expected, $this->extension->isSelectedChoice($choice, $value));
}
protected function renderForm(FormView $view, array $vars = array())
{
return (string) $this->extension->renderer->renderBlock($view, 'form', $vars);
}
protected function renderEnctype(FormView $view)
{
return (string) $this->extension->renderer->searchAndRenderBlock($view, 'enctype');
@ -173,6 +178,16 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
return (string) $this->extension->renderer->searchAndRenderBlock($view, 'rest', $vars);
}
protected function renderStart(FormView $view, array $vars = array())
{
return (string) $this->extension->renderer->renderBlock($view, 'form_start', $vars);
}
protected function renderEnd(FormView $view, array $vars = array())
{
return (string) $this->extension->renderer->renderBlock($view, 'form_end', $vars);
}
protected function setTheme(FormView $view, array $themes)
{
$this->extension->renderer->setTheme($view, $themes);

View File

@ -75,6 +75,11 @@ class FormExtensionTableLayoutTest extends AbstractTableLayoutTest
$this->extension = null;
}
protected function renderForm(FormView $view, array $vars = array())
{
return (string) $this->extension->renderer->renderBlock($view, 'form', $vars);
}
protected function renderEnctype(FormView $view)
{
return (string) $this->extension->renderer->searchAndRenderBlock($view, 'enctype');
@ -109,6 +114,16 @@ class FormExtensionTableLayoutTest extends AbstractTableLayoutTest
return (string) $this->extension->renderer->searchAndRenderBlock($view, 'rest', $vars);
}
protected function renderStart(FormView $view, array $vars = array())
{
return (string) $this->extension->renderer->renderBlock($view, 'form_start', $vars);
}
protected function renderEnd(FormView $view, array $vars = array())
{
return (string) $this->extension->renderer->renderBlock($view, 'form_end', $vars);
}
protected function setTheme(FormView $view, array $themes)
{
$this->extension->renderer->setTheme($view, $themes);

View File

@ -10,6 +10,9 @@ CHANGELOG
* added `TimedPhpEngine`
* added `--clean` option the the `translation:update` command
* added `http_method_override` option
* added support for default templates per render tag
* added FormHelper::form(), FormHelper::start() and FormHelper::end()
* deprecated FormHelper::enctype() in favor of FormHelper::start()
2.2.0
-----
@ -27,10 +30,10 @@ CHANGELOG
* replaced Symfony\Bundle\FrameworkBundle\Controller\TraceableControllerResolver by Symfony\Component\HttpKernel\Controller\TraceableControllerResolver
* replaced Symfony\Component\HttpKernel\Debug\ContainerAwareTraceableEventDispatcher by Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher
* added Client::enableProfiler()
* A new parameter has been added to the DIC: `router.request_context.base_url`
* a new parameter has been added to the DIC: `router.request_context.base_url`
You can customize it for your functional tests or for generating urls with
the right base url when your are in the cli context.
* Added support for default templates per render tag
* added support for default templates per render tag
2.1.0
-----

View File

@ -0,0 +1,3 @@
<?php echo $view['form']->start($form) ?>
<?php echo $view['form']->widget($form) ?>
<?php echo $view['form']->end($form) ?>

View File

@ -0,0 +1,4 @@
<?php if (!isset($render_rest) || $render_rest): ?>
<?php echo $view['form']->rest($form) ?>
<?php endif ?>
</form>

View File

@ -0,0 +1,6 @@
<?php $method = strtoupper($method) ?>
<?php $form_method = $method === 'GET' || $method === 'POST' ? $method : 'POST' ?>
<form method="<?php echo strtolower($form_method) ?>" action="<?php echo $action ?>"<?php foreach ($attr as $k => $v) { printf(' %s="%s"', $view->escape($k), $view->escape($v)); } ?><?php if ($multipart): ?> enctype="multipart/form-data"<?php endif ?>>
<?php if ($form_method !== $method): ?>
<input type="hidden" name="_method" value="<?php echo $method ?>" />
<?php endif ?>

View File

@ -57,19 +57,88 @@ class FormHelper extends Helper
$this->renderer->setTheme($view, $themes);
}
/**
* Renders the HTML for a form.
*
* Example usage:
*
* <?php echo view['form']->form($form) ?>
*
* You can pass options during the call:
*
* <?php echo view['form']->form($form, array('attr' => array('class' => 'foo'))) ?>
*
* <?php echo view['form']->form($form, array('separator' => '+++++')) ?>
*
* This method is mainly intended for prototyping purposes. If you want to
* control the layout of a form in a more fine-grained manner, you are
* advised to use the other helper methods for rendering the parts of the
* form individually. You can also create a custom form theme to adapt
* the look of the form.
*
* @param FormView $view The view for which to render the form
* @param array $variables Additional variables passed to the template
*
* @return string The HTML markup
*/
public function form(FormView $view, array $variables = array())
{
return $this->renderer->renderBlock($view, 'form', $variables);
}
/**
* Renders the form start tag.
*
* Example usage templates:
*
* <?php echo $view['form']->start($form) ?>>
*
* @param FormView $view The view for which to render the start tag
* @param array $variables Additional variables passed to the template
*
* @return string The HTML markup
*/
public function start(FormView $view, array $variables = array())
{
return $this->renderer->renderBlock($view, 'form_start', $variables);
}
/**
* Renders the form end tag.
*
* Example usage templates:
*
* <?php echo $view['form']->end($form) ?>>
*
* @param FormView $view The view for which to render the end tag
* @param array $variables Additional variables passed to the template
*
* @return string The HTML markup
*/
public function end(FormView $view, array $variables = array())
{
return $this->renderer->renderBlock($view, 'form_end', $variables);
}
/**
* Renders the HTML enctype in the form tag, if necessary.
*
* Example usage templates:
*
* <form action="..." method="post" <?php echo $view['form']->enctype() ?>>
* <form action="..." method="post" <?php echo $view['form']->enctype($form) ?>>
*
* @param FormView $view The view for which to render the encoding type
*
* @return string The HTML markup
*
* @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
* {@link start} instead.
*/
public function enctype(FormView $view)
{
// Uncomment this as soon as the deprecation note should be shown
// trigger_error('The form helper $view[\'form\']->enctype() is deprecated since version 2.3 and will be removed in 3.0. Use $view[\'form\']->start() instead.', E_USER_DEPRECATED);
return $this->renderer->searchAndRenderBlock($view, 'enctype');
}
@ -78,13 +147,13 @@ class FormHelper extends Helper
*
* Example usage:
*
* <?php echo view['form']->widget() ?>
* <?php echo view['form']->widget($form) ?>
*
* You can pass options during the call:
*
* <?php echo view['form']->widget(array('attr' => array('class' => 'foo'))) ?>
* <?php echo view['form']->widget($form, array('attr' => array('class' => 'foo'))) ?>
*
* <?php echo view['form']->widget(array('separator' => '+++++')) ?>
* <?php echo view['form']->widget($form, array('separator' => '+++++')) ?>
*
* @param FormView $view The view for which to render the widget
* @param array $variables Additional variables passed to the template

View File

@ -72,6 +72,11 @@ class FormHelperDivLayoutTest extends AbstractDivLayoutTest
parent::tearDown();
}
protected function renderForm(FormView $view, array $vars = array())
{
return (string) $this->engine->get('form')->form($view, $vars);
}
protected function renderEnctype(FormView $view)
{
return (string) $this->engine->get('form')->enctype($view);
@ -102,6 +107,16 @@ class FormHelperDivLayoutTest extends AbstractDivLayoutTest
return (string) $this->engine->get('form')->rest($view, $vars);
}
protected function renderStart(FormView $view, array $vars = array())
{
return (string) $this->engine->get('form')->start($view, $vars);
}
protected function renderEnd(FormView $view, array $vars = array())
{
return (string) $this->engine->get('form')->end($view, $vars);
}
protected function setTheme(FormView $view, array $themes)
{
$this->engine->get('form')->setTheme($view, $themes);

View File

@ -73,6 +73,11 @@ class FormHelperTableLayoutTest extends AbstractTableLayoutTest
parent::tearDown();
}
protected function renderForm(FormView $view, array $vars = array())
{
return (string) $this->engine->get('form')->form($view, $vars);
}
protected function renderEnctype(FormView $view)
{
return (string) $this->engine->get('form')->enctype($view);
@ -103,6 +108,16 @@ class FormHelperTableLayoutTest extends AbstractTableLayoutTest
return (string) $this->engine->get('form')->rest($view, $vars);
}
protected function renderStart(FormView $view, array $vars = array())
{
return (string) $this->engine->get('form')->start($view, $vars);
}
protected function renderEnd(FormView $view, array $vars = array())
{
return (string) $this->engine->get('form')->end($view, $vars);
}
protected function setTheme(FormView $view, array $themes)
{
$this->engine->get('form')->setTheme($view, $themes);

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form;
use Symfony\Component\Form\Exception\AlreadyBoundException;
use Symfony\Component\Form\Exception\BadMethodCallException;
/**
* A form button.
@ -342,6 +343,18 @@ class Button implements \IteratorAggregate, FormInterface
return true;
}
/**
* Unsupported method.
*
* @param mixed $request
*
* @throws BadMethodCallException
*/
public function process($request = null)
{
throw new BadMethodCallException('Buttons cannot be processed. Call process() on the root form instead.');
}
/**
* Binds data to the button.
*

View File

@ -456,6 +456,42 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
throw new \BadMethodCallException('Buttons do not support form factories.');
}
/**
* Unsupported method.
*
* @param string $action
*
* @throws \BadMethodCallException
*/
public function setAction($action)
{
throw new \BadMethodCallException('Buttons do not support actions.');
}
/**
* Unsupported method.
*
* @param string $method
*
* @throws \BadMethodCallException
*/
public function setMethod($method)
{
throw new \BadMethodCallException('Buttons do not support methods.');
}
/**
* Unsupported method.
*
* @param FormProcessorInterface $formProcessor
*
* @throws \BadMethodCallException
*/
public function setFormProcessor(FormProcessorInterface $formProcessor)
{
throw new \BadMethodCallException('Buttons do not support form processors.');
}
/**
* Builds and returns the button configuration.
*
@ -693,6 +729,36 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
return null;
}
/**
* Unsupported method.
*
* @return null Always returns null.
*/
public function getAction()
{
return null;
}
/**
* Unsupported method.
*
* @return null Always returns null.
*/
public function getMethod()
{
return null;
}
/**
* Unsupported method.
*
* @return null Always returns null.
*/
public function getFormProcessor()
{
return null;
}
/**
* Returns all options passed during the construction of the button.
*

View File

@ -6,6 +6,9 @@ CHANGELOG
------
* changed FormRenderer::humanize() to humanize also camel cased field name
* added FormProcessorInterface and FormInterface::process()
* deprecated passing a Request instance to FormInterface::bind()
* added options "method" and "action" to FormType
2.2.0
-----

View File

@ -53,6 +53,8 @@ class FormType extends BaseType
->setData(isset($options['data']) ? $options['data'] : null)
->setDataLocked(isset($options['data']))
->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null)
->setMethod($options['method'])
->setAction($options['action'])
;
if ($options['trim']) {
@ -93,6 +95,8 @@ class FormType extends BaseType
'size' => null,
'label_attr' => $options['label_attr'],
'compound' => $form->getConfig()->getCompound(),
'method' => $form->getConfig()->getMethod(),
'action' => $form->getConfig()->getAction(),
));
}
@ -167,6 +171,10 @@ class FormType extends BaseType
'label_attr' => array(),
'virtual' => false,
'compound' => true,
'method' => 'POST',
// According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt)
// section 4.2., empty URIs are considered same-document references
'action' => '',
));
$resolver->setAllowedTypes(array(

View File

@ -19,6 +19,9 @@ use Symfony\Component\HttpFoundation\Request;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since version 2.3, to be removed in 3.0. Pass the
* Request instance to {@link Form::process()} instead.
*/
class BindRequestListener implements EventSubscriberInterface
{
@ -40,6 +43,9 @@ class BindRequestListener implements EventSubscriberInterface
return;
}
// Uncomment this as soon as the deprecation note should be shown
// trigger_error('Passing a Request instance to Form::bind() is deprecated since version 2.3 and will be disabled in 3.0. Call Form::process($request) instead.', E_USER_DEPRECATED);
$name = $form->getConfig()->getName();
$default = $form->getConfig()->getCompound() ? array() : null;

View File

@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\HttpFoundation;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormProcessorInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* A form processor using the {@link Request} class of the HttpFoundation
* component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RequestFormProcessor implements FormProcessorInterface
{
/**
* {@inheritdoc}
*/
public function processForm(FormInterface $form, $request = null)
{
if (!$request instanceof Request) {
throw new UnexpectedTypeException($request, 'Symfony\Component\HttpFoundation\Request');
}
$name = $form->getName();
$method = $form->getConfig()->getMethod();
if ($method !== $request->getMethod()) {
return;
}
if ('GET' === $method) {
if ('' === $name) {
$data = $request->query->all();
} else {
// Don't bind GET requests if the form's name does not exist
// in the request
if (!$request->query->has($name)) {
return;
}
$data = $request->query->get($name);
}
} else {
if ('' === $name) {
$params = $request->request->all();
$files = $request->files->all();
} else {
$default = $form->getConfig()->getCompound() ? array() : null;
$params = $request->request->get($name, $default);
$files = $request->files->get($name, $default);
}
if (is_array($params) && is_array($files)) {
$data = array_replace_recursive($params, $files);
} else {
$data = $params ?: $files;
}
}
// Don't auto-bind the form unless at least one field is submitted.
if ('' === $name && count(array_intersect_key($data, $form->all())) <= 0) {
return;
}
$form->bind($data);
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Extension\HttpFoundation\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\HttpFoundation\EventListener\BindRequestListener;
use Symfony\Component\Form\Extension\HttpFoundation\RequestFormProcessor;
use Symfony\Component\Form\FormBuilderInterface;
/**
@ -25,9 +26,15 @@ class FormTypeHttpFoundationExtension extends AbstractTypeExtension
*/
private $listener;
/**
* @var RequestFormProcessor
*/
private $processor;
public function __construct()
{
$this->listener = new BindRequestListener();
$this->processor = new RequestFormProcessor();
}
/**
@ -36,6 +43,7 @@ class FormTypeHttpFoundationExtension extends AbstractTypeExtension
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventSubscriber($this->listener);
$builder->setFormProcessor($this->processor);
}
/**

View File

@ -404,6 +404,16 @@ class Form implements \IteratorAggregate, FormInterface
return $this->extraData;
}
/**
* {@inheritdoc}
*/
public function process($request = null)
{
$this->config->getFormProcessor()->processForm($this, $request);
return $this;
}
/**
* {@inheritdoc}
*/
@ -579,7 +589,7 @@ class Form implements \IteratorAggregate, FormInterface
public function isValid()
{
if (!$this->bound) {
throw new \LogicException('You cannot call isValid() on a form that is not bound.');
return false;
}
if (count($this->errors) > 0) {

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Form;
use Symfony\Component\Form\Exception\BadMethodCallException;
use Symfony\Component\Form\Exception\Exception;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
@ -27,6 +27,26 @@ use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
*/
class FormConfigBuilder implements FormConfigBuilderInterface
{
/**
* Caches a globally unique {@link NativeFormProcessor} instance.
*
* @var NativeFormProcessor
*/
private static $nativeFormProcessor;
/**
* The accepted request methods.
*
* @var array
*/
private static $allowedMethods = array(
'GET',
'PUT',
'POST',
'DELETE',
'PATCH'
);
/**
* @var Boolean
*/
@ -137,6 +157,21 @@ class FormConfigBuilder implements FormConfigBuilderInterface
*/
private $formFactory;
/**
* @var string
*/
private $action;
/**
* @var string
*/
private $method = 'POST';
/**
* @var FormProcessorInterface
*/
private $formProcessor;
/**
* @var array
*/
@ -264,6 +299,10 @@ class FormConfigBuilder implements FormConfigBuilderInterface
*/
public function getEventDispatcher()
{
if ($this->locked && !$this->dispatcher instanceof ImmutableEventDispatcher) {
$this->dispatcher = new ImmutableEventDispatcher($this->dispatcher);
}
return $this->dispatcher;
}
@ -435,6 +474,37 @@ class FormConfigBuilder implements FormConfigBuilderInterface
return $this->formFactory;
}
/**
* {@inheritdoc}
*/
public function getAction()
{
return $this->action;
}
/**
* {@inheritdoc}
*/
public function getMethod()
{
return $this->method;
}
/**
* {@inheritdoc}
*/
public function getFormProcessor()
{
if (null === $this->formProcessor) {
if (null === self::$nativeFormProcessor) {
self::$nativeFormProcessor = new NativeFormProcessor();
}
$this->formProcessor = self::$nativeFormProcessor;
}
return $this->formProcessor;
}
/**
* {@inheritdoc}
*/
@ -687,6 +757,58 @@ class FormConfigBuilder implements FormConfigBuilderInterface
return $this;
}
/**
* {@inheritdoc}
*/
public function setAction($action)
{
if ($this->locked) {
throw new BadMethodCallException('The config builder cannot be modified anymore.');
}
$this->action = $action;
return $this;
}
/**
* {@inheritdoc}
*/
public function setMethod($method)
{
if ($this->locked) {
throw new BadMethodCallException('The config builder cannot be modified anymore.');
}
$upperCaseMethod = strtoupper($method);
if (!in_array($upperCaseMethod, self::$allowedMethods)) {
throw new InvalidArgumentException(sprintf(
'The form method is "%s", but should be one of "%s".',
$method,
implode('", "', self::$allowedMethods)
));
}
$this->method = $upperCaseMethod;
return $this;
}
/**
* {@inheritdoc}
*/
public function setFormProcessor(FormProcessorInterface $formProcessor)
{
if ($this->locked) {
throw new BadMethodCallException('The config builder cannot be modified anymore.');
}
$this->formProcessor = $formProcessor;
return $this;
}
/**
* {@inheritdoc}
*/
@ -700,10 +822,6 @@ class FormConfigBuilder implements FormConfigBuilderInterface
$config = clone $this;
$config->locked = true;
if (!$config->dispatcher instanceof ImmutableEventDispatcher) {
$config->dispatcher = new ImmutableEventDispatcher($config->dispatcher);
}
return $config;
}

View File

@ -237,6 +237,31 @@ interface FormConfigBuilderInterface extends FormConfigInterface
*/
public function setFormFactory(FormFactoryInterface $formFactory);
/**
* Sets the target URL of the form.
*
* @param string $action The target URL of the form.
*
* @return self The configuration object.
*/
public function setAction($action);
/**
* Sets the HTTP method used by the form.
*
* @param string $method The HTTP method of the form.
*
* @return self The configuration object.
*/
public function setMethod($method);
/**
* @param FormProcessorInterface $formProcessor
*
* @return self The configuration object.
*/
public function setFormProcessor(FormProcessorInterface $formProcessor);
/**
* Builds and returns the form configuration.
*

View File

@ -190,6 +190,25 @@ interface FormConfigInterface
*/
public function getFormFactory();
/**
* Returns the target URL of the form.
*
* @return string The target URL of the form.
*/
public function getAction();
/**
* Returns the HTTP method used by the form.
*
* @return string The HTTP method of the form.
*/
public function getMethod();
/**
* @return FormProcessorInterface The form processor.
*/
public function getFormProcessor();
/**
* Returns all options passed during the construction of the form.
*

View File

@ -182,6 +182,8 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
/**
* Returns whether the form and all children are valid.
*
* If the form is not bound, this method always returns false.
*
* @return Boolean
*/
public function isValid();
@ -224,6 +226,19 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
*/
public function isSynchronized();
/**
* Processes the given request and binds the form if it was submitted.
*
* Internally, the request is forwarded to a {@link FormProcessorInterface}
* instance. This instance determines the allowed value of the
* $request parameter.
*
* @param mixed $request The request to check.
*
* @return FormInterface The form instance.
*/
public function process($request = null);
/**
* Binds data to the form, transforms and validates it.
*

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
/**
* Binds forms from requests if they were submitted.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface FormProcessorInterface
{
/**
* Binds a form from a request if it was submitted.
*
* @param FormInterface $form The form to bind.
* @param mixed $request The current request.
*/
public function processForm(FormInterface $form, $request = null);
}

View File

@ -87,19 +87,31 @@ class FormRenderer implements FormRendererInterface
*/
public function renderBlock(FormView $view, $blockName, array $variables = array())
{
if (0 == count($this->variableStack)) {
throw new Exception('This method should only be called while rendering a form element.');
}
$viewCacheKey = $view->vars[self::CACHE_KEY_VAR];
$scopeVariables = end($this->variableStack[$viewCacheKey]);
$resource = $this->engine->getResourceForBlockName($view, $blockName);
if (!$resource) {
throw new Exception(sprintf('No block "%s" found while rendering the form.', $blockName));
}
$viewCacheKey = $view->vars[self::CACHE_KEY_VAR];
// The variables are cached globally for a view (instead of for the
// current suffix)
if (!isset($this->variableStack[$viewCacheKey])) {
$this->variableStack[$viewCacheKey] = array();
// The default variable scope contains all view variables, merged with
// the variables passed explicitly to the helper
$scopeVariables = $view->vars;
$varInit = true;
} else {
// Reuse the current scope and merge it with the explicitly passed variables
$scopeVariables = end($this->variableStack[$viewCacheKey]);
$varInit = false;
}
// Merge the passed with the existing attributes
if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
$variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
@ -122,6 +134,10 @@ class FormRenderer implements FormRendererInterface
// Clear the stack
array_pop($this->variableStack[$viewCacheKey]);
if ($varInit) {
unset($this->variableStack[$viewCacheKey]);
}
return $html;
}
@ -191,6 +207,8 @@ class FormRenderer implements FormRendererInterface
// The variables are cached globally for a view (instead of for the
// current suffix)
if (!isset($this->variableStack[$viewCacheKey])) {
$this->variableStack[$viewCacheKey] = array();
// The default variable scope contains all view variables, merged with
// the variables passed explicitly to the helper
$scopeVariables = $view->vars;

View File

@ -0,0 +1,194 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormProcessorInterface;
/**
* A form processor using PHP's super globals $_GET, $_POST and $_SERVER.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NativeFormProcessor implements FormProcessorInterface
{
/**
* The allowed keys of the $_FILES array.
*
* @var array
*/
private static $fileKeys = array(
'error',
'name',
'size',
'tmp_name',
'type',
);
/**
* {@inheritdoc}
*/
public function processForm(FormInterface $form, $request = null)
{
if (null !== $request) {
throw new UnexpectedTypeException($request, 'null');
}
$name = $form->getName();
$method = $form->getConfig()->getMethod();
if ($method !== self::getRequestMethod()) {
return;
}
if ('GET' === $method) {
if ('' === $name) {
$data = $_GET;
} else {
// Don't bind GET requests if the form's name does not exist
// in the request
if (!isset($_GET[$name])) {
return;
}
$data = $_GET[$name];
}
} else {
$fixedFiles = array();
foreach ($_FILES as $name => $file) {
$fixedFiles[$name] = self::stripEmptyFiles(self::fixPhpFilesArray($file));
}
if ('' === $name) {
$params = $_POST;
$files = $fixedFiles;
} else {
$default = $form->getConfig()->getCompound() ? array() : null;
$params = isset($_POST[$name]) ? $_POST[$name] : $default;
$files = isset($fixedFiles[$name]) ? $fixedFiles[$name] : $default;
}
if (is_array($params) && is_array($files)) {
$data = array_replace_recursive($params, $files);
} else {
$data = $params ?: $files;
}
}
// Don't auto-bind the form unless at least one field is submitted.
if ('' === $name && count(array_intersect_key($data, $form->all())) <= 0) {
return;
}
$form->bind($data);
}
/**
* Returns the method used to submit the request to the server.
*
* @return string The request method.
*/
private static function getRequestMethod()
{
$method = isset($_SERVER['REQUEST_METHOD'])
? strtoupper($_SERVER['REQUEST_METHOD'])
: 'GET';
if ('POST' === $method && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
}
return $method;
}
/**
* Fixes a malformed PHP $_FILES array.
*
* PHP has a bug that the format of the $_FILES array differs, depending on
* whether the uploaded file fields had normal field names or array-like
* field names ("normal" vs. "parent[child]").
*
* This method fixes the array to look like the "normal" $_FILES array.
*
* It's safe to pass an already converted array, in which case this method
* just returns the original array unmodified.
*
* This method is identical to {@link Symfony\Component\HttpFoundation\FileBag::fixPhpFilesArray}
* and should be kept as such in order to port fixes quickly and easily.
*
* @param array $data
*
* @return array
*/
private static function fixPhpFilesArray($data)
{
if (!is_array($data)) {
return $data;
}
$keys = array_keys($data);
sort($keys);
if (self::$fileKeys !== $keys || !isset($data['name']) || !is_array($data['name'])) {
return $data;
}
$files = $data;
foreach (self::$fileKeys as $k) {
unset($files[$k]);
}
foreach (array_keys($data['name']) as $key) {
$files[$key] = self::fixPhpFilesArray(array(
'error' => $data['error'][$key],
'name' => $data['name'][$key],
'type' => $data['type'][$key],
'tmp_name' => $data['tmp_name'][$key],
'size' => $data['size'][$key]
));
}
return $files;
}
/**
* Sets empty uploaded files to NULL in the given uploaded files array.
*
* @param mixed $data The file upload data.
*
* @return array|null Returns the stripped upload data.
*/
private static function stripEmptyFiles($data)
{
if (!is_array($data)) {
return $data;
}
$keys = array_keys($data);
sort($keys);
if (self::$fileKeys === $keys) {
if (UPLOAD_ERR_NO_FILE === $data['error']) {
return null;
}
return $data;
}
foreach ($data as $key => $value) {
$data[$key] = self::stripEmptyFiles($value);
}
return $data;
}
}

View File

@ -2,6 +2,8 @@
namespace Symfony\Component\Form\Test;
use Symfony\Component\Form\FormEvent;
class DeprecationErrorHandler
{
public static function handle($errorNumber, $message, $file, $line, $context)
@ -21,4 +23,11 @@ class DeprecationErrorHandler
return false;
}
public static function preBind($listener, FormEvent $event)
{
set_error_handler(array('Symfony\Component\Form\Test\DeprecationErrorHandler', 'handle'));
$listener->preBind($event);
restore_error_handler();
}
}

View File

@ -378,6 +378,50 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
}
public function testForm()
{
$form = $this->factory->createNamedBuilder('name', 'form')
->setMethod('PUT')
->setAction('http://example.com')
->add('firstName', 'text')
->add('lastName', 'text')
->getForm();
// include ampersands everywhere to validate escaping
$html = $this->renderForm($form->createView(), array(
'id' => 'my&id',
'attr' => array('class' => 'my&class'),
));
$this->assertMatchesXpath($html,
'/form
[
./input[@type="hidden"][@name="_method"][@value="PUT"]
/following-sibling::div
[
./div
[
./label[@for="name_firstName"]
/following-sibling::input[@type="text"][@id="name_firstName"]
]
/following-sibling::div
[
./label[@for="name_lastName"]
/following-sibling::input[@type="text"][@id="name_lastName"]
]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
[count(.//input)=3]
[@id="my&id"]
[@class="my&class"]
]
[@method="post"]
[@action="http://example.com"]
[@class="my&class"]
'
);
}
public function testFormWidget()
{
$form = $this->factory->createNamedBuilder('name', 'form')
->add('firstName', 'text')
@ -642,4 +686,50 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
'
);
}
public function testFormEndWithRest()
{
$view = $this->factory->createNamedBuilder('name', 'form')
->add('field1', 'text')
->add('field2', 'text')
->getForm()
->createView();
$this->renderWidget($view['field1']);
// Rest should only contain field2
$html = $this->renderEnd($view);
// Insert the start tag, the end tag should be rendered by the helper
$this->assertMatchesXpath('<form>' . $html,
'/form
[
./div
[
./label[@for="name_field2"]
/following-sibling::input[@type="text"][@id="name_field2"]
]
/following-sibling::input
[@type="hidden"]
[@id="name__token"]
]
'
);
}
public function testFormEndWithoutRest()
{
$view = $this->factory->createNamedBuilder('name', 'form')
->add('field1', 'text')
->add('field2', 'text')
->getForm()
->createView();
$this->renderWidget($view['field1']);
// Rest should only contain field2, but isn't rendered
$html = $this->renderEnd($view, array('render_rest' => false));
$this->assertEquals('</form>', $html);
}
}

View File

@ -0,0 +1,280 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class AbstractFormProcessorTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \Symfony\Component\Form\FormProcessorInterface
*/
protected $processor;
protected $request;
protected function setUp()
{
$this->processor = $this->getFormProcessor();
$this->request = null;
}
public function methodExceptGetProvider()
{
return array(
array('POST'),
array('PUT'),
array('DELETE'),
array('PATCH'),
);
}
public function methodProvider()
{
return array_merge(array(
array('GET'),
), $this->methodExceptGetProvider());
}
/**
* @dataProvider methodProvider
*/
public function testBindIfNameInRequest($method)
{
$form = $this->getMockForm('param1', $method);
$this->setRequestData($method, array(
'param1' => 'DATA',
));
$form->expects($this->once())
->method('bind')
->with('DATA');
$this->processor->processForm($form, $this->request);
}
/**
* @dataProvider methodProvider
*/
public function testDoNotBindIfWrongRequestMethod($method)
{
$form = $this->getMockForm('param1', $method);
$otherMethod = 'POST' === $method ? 'PUT' : 'POST';
$this->setRequestData($otherMethod, array(
'param1' => 'DATA',
));
$form->expects($this->never())
->method('bind');
$this->processor->processForm($form, $this->request);
}
/**
* @dataProvider methodExceptGetProvider
*/
public function testBindSimpleFormWithNullIfNameNotInRequestAndNotGetRequest($method)
{
$form = $this->getMockForm('param1', $method, false);
$this->setRequestData($method, array(
'paramx' => array(),
));
$form->expects($this->once())
->method('bind')
->with($this->identicalTo(null));
$this->processor->processForm($form, $this->request);
}
/**
* @dataProvider methodExceptGetProvider
*/
public function testBindCompoundFormWithArrayIfNameNotInRequestAndNotGetRequest($method)
{
$form = $this->getMockForm('param1', $method, true);
$this->setRequestData($method, array(
'paramx' => array(),
));
$form->expects($this->once())
->method('bind')
->with($this->identicalTo(array()));
$this->processor->processForm($form, $this->request);
}
public function testDoNotBindIfNameNotInRequestAndGetRequest()
{
$form = $this->getMockForm('param1', 'GET');
$this->setRequestData('GET', array(
'paramx' => array(),
));
$form->expects($this->never())
->method('bind');
$this->processor->processForm($form, $this->request);
}
/**
* @dataProvider methodProvider
*/
public function testBindFormWithEmptyNameIfAtLeastOneFieldInRequest($method)
{
$form = $this->getMockForm('', $method);
$form->expects($this->any())
->method('all')
->will($this->returnValue(array(
'param1' => $this->getMockForm('param1'),
'param2' => $this->getMockForm('param2'),
)));
$this->setRequestData($method, $requestData = array(
'param1' => 'submitted value',
'paramx' => 'submitted value',
));
$form->expects($this->once())
->method('bind')
->with($requestData);
$this->processor->processForm($form, $this->request);
}
/**
* @dataProvider methodProvider
*/
public function testDoNotBindFormWithEmptyNameIfNoFieldInRequest($method)
{
$form = $this->getMockForm('', $method);
$form->expects($this->any())
->method('all')
->will($this->returnValue(array(
'param1' => $this->getMockForm('param1'),
'param2' => $this->getMockForm('param2'),
)));
$this->setRequestData($method, array(
'paramx' => 'submitted value',
));
$form->expects($this->never())
->method('bind');
$this->processor->processForm($form, $this->request);
}
/**
* @dataProvider methodExceptGetProvider
*/
public function testMergeParamsAndFiles($method)
{
$form = $this->getMockForm('param1', $method);
$file = $this->getMockFile();
$this->setRequestData($method, array(
'param1' => array(
'field1' => 'DATA',
),
), array(
'param1' => array(
'field2' => $file,
),
));
$form->expects($this->once())
->method('bind')
->with(array(
'field1' => 'DATA',
'field2' => $file,
));
$this->processor->processForm($form, $this->request);
}
/**
* @dataProvider methodExceptGetProvider
*/
public function testParamTakesPrecedenceOverFile($method)
{
$form = $this->getMockForm('param1', $method);
$file = $this->getMockFile();
$this->setRequestData($method, array(
'param1' => 'DATA',
), array(
'param1' => $file,
));
$form->expects($this->once())
->method('bind')
->with('DATA');
$this->processor->processForm($form, $this->request);
}
/**
* @dataProvider methodExceptGetProvider
*/
public function testBindFileIfNoParam($method)
{
$form = $this->getMockForm('param1', $method);
$file = $this->getMockFile();
$this->setRequestData($method, array(
'param1' => null,
), array(
'param1' => $file,
));
$form->expects($this->once())
->method('bind')
->with($file);
$this->processor->processForm($form, $this->request);
}
abstract protected function setRequestData($method, $data, $files = array());
abstract protected function getFormProcessor();
abstract protected function getMockFile();
protected function getMockForm($name, $method = null, $compound = true)
{
$config = $this->getMock('Symfony\Component\Form\FormConfigInterface');
$config->expects($this->any())
->method('getMethod')
->will($this->returnValue($method));
$config->expects($this->any())
->method('getCompound')
->will($this->returnValue($compound));
$form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
$form->expects($this->any())
->method('getName')
->will($this->returnValue($name));
$form->expects($this->any())
->method('getConfig')
->will($this->returnValue($config));
return $form;
}
}

View File

@ -19,8 +19,6 @@ abstract class AbstractLayoutTest extends FormIntegrationTestCase
{
protected $csrfProvider;
protected $factory;
protected function setUp()
{
if (!extension_loaded('intl')) {
@ -44,7 +42,6 @@ abstract class AbstractLayoutTest extends FormIntegrationTestCase
protected function tearDown()
{
$this->csrfProvider = null;
$this->factory = null;
parent::tearDown();
}
@ -102,6 +99,8 @@ abstract class AbstractLayoutTest extends FormIntegrationTestCase
$this->assertMatchesXpath($html, $xpath);
}
abstract protected function renderForm(FormView $view, array $vars = array());
abstract protected function renderEnctype(FormView $view);
abstract protected function renderLabel(FormView $view, $label = null, array $vars = array());
@ -114,6 +113,10 @@ abstract class AbstractLayoutTest extends FormIntegrationTestCase
abstract protected function renderRest(FormView $view, array $vars = array());
abstract protected function renderStart(FormView $view, array $vars = array());
abstract protected function renderEnd(FormView $view, array $vars = array());
abstract protected function setTheme(FormView $view, array $themes);
public function testEnctype()
@ -1744,9 +1747,7 @@ abstract class AbstractLayoutTest extends FormIntegrationTestCase
$this->assertMatchesXpath($html,
'//div[@id="name_items"][@data-prototype]
|
//table[@id="name_items"][@data-prototype]
'
//table[@id="name_items"][@data-prototype]'
);
}
@ -1796,4 +1797,76 @@ abstract class AbstractLayoutTest extends FormIntegrationTestCase
'/button[@type="reset"][@name="name"]'
);
}
public function testStartTag()
{
$form = $this->factory->create('form', null, array(
'method' => 'get',
'action' => 'http://example.com/directory'
));
$html = $this->renderStart($form->createView());
$this->assertSame('<form method="get" action="http://example.com/directory">', $html);
}
public function testStartTagForPutRequest()
{
$form = $this->factory->create('form', null, array(
'method' => 'put',
'action' => 'http://example.com/directory'
));
$html = $this->renderStart($form->createView());
$this->assertMatchesXpath($html . '</form>',
'/form
[./input[@type="hidden"][@name="_method"][@value="PUT"]]
[@method="post"]
[@action="http://example.com/directory"]'
);
}
public function testStartTagWithOverriddenVars()
{
$form = $this->factory->create('form', null, array(
'method' => 'put',
'action' => 'http://example.com/directory',
));
$html = $this->renderStart($form->createView(), array(
'method' => 'post',
'action' => 'http://foo.com/directory'
));
$this->assertSame('<form method="post" action="http://foo.com/directory">', $html);
}
public function testStartTagForMultipartForm()
{
$form = $this->factory->createBuilder('form', null, array(
'method' => 'get',
'action' => 'http://example.com/directory'
))
->add('file', 'file')
->getForm();
$html = $this->renderStart($form->createView());
$this->assertSame('<form method="get" action="http://example.com/directory" enctype="multipart/form-data">', $html);
}
public function testStartTagWithExtraAttributes()
{
$form = $this->factory->create('form', null, array(
'method' => 'get',
'action' => 'http://example.com/directory'
));
$html = $this->renderStart($form->createView(), array(
'attr' => array('class' => 'foobar'),
));
$this->assertSame('<form method="get" action="http://example.com/directory" class="foobar">', $html);
}
}

View File

@ -224,6 +224,58 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
}
public function testForm()
{
$view = $this->factory->createNamedBuilder('name', 'form')
->setMethod('PUT')
->setAction('http://example.com')
->add('firstName', 'text')
->add('lastName', 'text')
->getForm()
->createView();
$html = $this->renderForm($view, array(
'id' => 'my&id',
'attr' => array('class' => 'my&class'),
));
$this->assertMatchesXpath($html,
'/form
[
./input[@type="hidden"][@name="_method"][@value="PUT"]
/following-sibling::table
[
./tr
[
./td
[./label[@for="name_firstName"]]
/following-sibling::td
[./input[@id="name_firstName"]]
]
/following-sibling::tr
[
./td
[./label[@for="name_lastName"]]
/following-sibling::td
[./input[@id="name_lastName"]]
]
/following-sibling::tr[@style="display: none"]
[./td[@colspan="2"]/input
[@type="hidden"]
[@id="name__token"]
]
]
[count(.//input)=3]
[@id="my&id"]
[@class="my&class"]
]
[@method="post"]
[@action="http://example.com"]
[@class="my&class"]
'
);
}
public function testFormWidget()
{
$view = $this->factory->createNamedBuilder('name', 'form')
->add('firstName', 'text')
@ -400,4 +452,58 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
'
);
}
public function testFormEndWithRest()
{
$view = $this->factory->createNamedBuilder('name', 'form')
->add('field1', 'text')
->add('field2', 'text')
->getForm()
->createView();
$this->renderWidget($view['field1']);
// Rest should only contain field2
$html = $this->renderEnd($view);
// Insert the start tag, the end tag should be rendered by the helper
// Unfortunately this is not valid HTML, because the surrounding table
// tag is missing. If someone renders a form with table layout
// manually, she should call form_rest() explicitly within the <table>
// tag.
$this->assertMatchesXpath('<form>' . $html,
'/form
[
./tr
[
./td
[./label[@for="name_field2"]]
/following-sibling::td
[./input[@id="name_field2"]]
]
/following-sibling::tr[@style="display: none"]
[./td[@colspan="2"]/input
[@type="hidden"]
[@id="name__token"]
]
]
'
);
}
public function testFormEndWithoutRest()
{
$view = $this->factory->createNamedBuilder('name', 'form')
->add('field1', 'text')
->add('field2', 'text')
->getForm()
->createView();
$this->renderWidget($view['field1']);
// Rest should only contain field2, but isn't rendered
$html = $this->renderEnd($view, array('render_rest' => false));
$this->assertEquals('</form>', $html);
}
}

View File

@ -11,9 +11,8 @@
namespace Symfony\Component\Form\Tests;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\Extension\HttpFoundation\RequestFormProcessor;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Extension\HttpFoundation\EventListener\BindRequestListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer;
@ -421,14 +420,15 @@ class CompoundFormTest extends AbstractFormTest
));
$form = $this->getBuilder('author')
->setMethod($method)
->setCompound(true)
->setDataMapper($this->getDataMapper())
->addEventSubscriber(new BindRequestListener())
->setFormProcessor(new RequestFormProcessor())
->getForm();
$form->add($this->getBuilder('name')->getForm());
$form->add($this->getBuilder('image')->getForm());
$form->bind($request);
$form->process($request);
$file = new UploadedFile($path, 'upload.png', 'image/png', 123, UPLOAD_ERR_OK);
@ -470,14 +470,15 @@ class CompoundFormTest extends AbstractFormTest
));
$form = $this->getBuilder('')
->setMethod($method)
->setCompound(true)
->setDataMapper($this->getDataMapper())
->addEventSubscriber(new BindRequestListener())
->setFormProcessor(new RequestFormProcessor())
->getForm();
$form->add($this->getBuilder('name')->getForm());
$form->add($this->getBuilder('image')->getForm());
$form->bind($request);
$form->process($request);
$file = new UploadedFile($path, 'upload.png', 'image/png', 123, UPLOAD_ERR_OK);
@ -515,10 +516,11 @@ class CompoundFormTest extends AbstractFormTest
));
$form = $this->getBuilder('image')
->addEventSubscriber(new BindRequestListener())
->setMethod($method)
->setFormProcessor(new RequestFormProcessor())
->getForm();
$form->bind($request);
$form->process($request);
$file = new UploadedFile($path, 'upload.png', 'image/png', 123, UPLOAD_ERR_OK);
@ -548,10 +550,11 @@ class CompoundFormTest extends AbstractFormTest
));
$form = $this->getBuilder('name')
->addEventSubscriber(new BindRequestListener())
->setMethod($method)
->setFormProcessor(new RequestFormProcessor())
->getForm();
$form->bind($request);
$form->process($request);
$this->assertEquals('Bernhard', $form->getData());
@ -576,14 +579,15 @@ class CompoundFormTest extends AbstractFormTest
));
$form = $this->getBuilder('author')
->setMethod('GET')
->setCompound(true)
->setDataMapper($this->getDataMapper())
->addEventSubscriber(new BindRequestListener())
->setFormProcessor(new RequestFormProcessor())
->getForm();
$form->add($this->getBuilder('firstName')->getForm());
$form->add($this->getBuilder('lastName')->getForm());
$form->bind($request);
$form->process($request);
$this->assertEquals('Bernhard', $form['firstName']->getData());
$this->assertEquals('Schussek', $form['lastName']->getData());
@ -606,14 +610,15 @@ class CompoundFormTest extends AbstractFormTest
));
$form = $this->getBuilder('')
->setMethod('GET')
->setCompound(true)
->setDataMapper($this->getDataMapper())
->addEventSubscriber(new BindRequestListener())
->setFormProcessor(new RequestFormProcessor())
->getForm();
$form->add($this->getBuilder('firstName')->getForm());
$form->add($this->getBuilder('lastName')->getForm());
$form->bind($request);
$form->process($request);
$this->assertEquals('Bernhard', $form['firstName']->getData());
$this->assertEquals('Schussek', $form['lastName']->getData());

View File

@ -15,6 +15,7 @@ use Symfony\Component\Form\Extension\HttpFoundation\EventListener\BindRequestLis
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormConfigBuilder;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Test\DeprecationErrorHandler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@ -101,7 +102,7 @@ class BindRequestListenerTest extends \PHPUnit_Framework_TestCase
$event = new FormEvent($form, $request);
$listener = new BindRequestListener();
$listener->preBind($event);
DeprecationErrorHandler::preBind($listener, $event);
$this->assertEquals(array(
'name' => 'Bernhard',
@ -128,7 +129,7 @@ class BindRequestListenerTest extends \PHPUnit_Framework_TestCase
$event = new FormEvent($form, $request);
$listener = new BindRequestListener();
$listener->preBind($event);
DeprecationErrorHandler::preBind($listener, $event);
$this->assertEquals(array(
'name' => 'Bernhard',
@ -157,7 +158,7 @@ class BindRequestListenerTest extends \PHPUnit_Framework_TestCase
$event = new FormEvent($form, $request);
$listener = new BindRequestListener();
$listener->preBind($event);
DeprecationErrorHandler::preBind($listener, $event);
// Default to empty array
$this->assertEquals(array(), $event->getData());
@ -183,7 +184,7 @@ class BindRequestListenerTest extends \PHPUnit_Framework_TestCase
$event = new FormEvent($form, $request);
$listener = new BindRequestListener();
$listener->preBind($event);
DeprecationErrorHandler::preBind($listener, $event);
// Default to null
$this->assertNull($event->getData());
@ -206,7 +207,7 @@ class BindRequestListenerTest extends \PHPUnit_Framework_TestCase
$event = new FormEvent($form, $request);
$listener = new BindRequestListener();
$listener->preBind($event);
DeprecationErrorHandler::preBind($listener, $event);
$this->assertEquals(array(
'name' => 'Bernhard',
@ -230,7 +231,7 @@ class BindRequestListenerTest extends \PHPUnit_Framework_TestCase
$event = new FormEvent($form, $request);
$listener = new BindRequestListener();
$listener->preBind($event);
DeprecationErrorHandler::preBind($listener, $event);
$this->assertEquals(array(
'name' => 'Bernhard',
@ -256,7 +257,7 @@ class BindRequestListenerTest extends \PHPUnit_Framework_TestCase
$event = new FormEvent($form, $request);
$listener = new BindRequestListener();
$listener->preBind($event);
DeprecationErrorHandler::preBind($listener, $event);
$this->assertEquals(array(), $event->getData());
}
@ -278,7 +279,7 @@ class BindRequestListenerTest extends \PHPUnit_Framework_TestCase
$event = new FormEvent($form, $request);
$listener = new BindRequestListener();
$listener->preBind($event);
DeprecationErrorHandler::preBind($listener, $event);
$this->assertNull($event->getData());
}

View File

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Extension\HttpFoundation;
use Symfony\Component\Form\Extension\HttpFoundation\RequestFormProcessor;
use Symfony\Component\Form\Tests\AbstractFormProcessorTest;
use Symfony\Component\HttpFoundation\Request;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RequestFormProcessorTest extends AbstractFormProcessorTest
{
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testRequestShouldNotBeNull()
{
$this->processor->processForm($this->getMockForm('name', 'GET'));
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testRequestShouldBeInstanceOfRequest()
{
$this->processor->processForm($this->getMockForm('name', 'GET'), new \stdClass());
}
protected function setRequestData($method, $data, $files = array())
{
$this->request = Request::create('http://localhost', $method, $data, array(), $files);
}
protected function getFormProcessor()
{
return new RequestFormProcessor();
}
protected function getMockFile()
{
return $this->getMockBuilder('Symfony\Component\HttpFoundation\File\UploadedFile')
->disableOriginalConstructor()
->getMock();
}
}

View File

@ -12,12 +12,11 @@
namespace Symfony\Component\Form\Tests;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\FormConfigBuilder;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
use Symfony\Component\Form\FormConfigBuilder;
class FormConfigTest extends \PHPUnit_Framework_TestCase
{
public function getHtml4Ids()
@ -90,4 +89,59 @@ class FormConfigTest extends \PHPUnit_Framework_TestCase
}
}
}
public function testGetFormProcessorCreatesNativeFormProcessorIfNotSet()
{
$config = $this->getConfigBuilder()->getFormConfig();
$this->assertInstanceOf('Symfony\Component\Form\NativeFormProcessor', $config->getFormProcessor());
}
public function testGetFormProcessorReusesNativeFormProcessorInstance()
{
$config1 = $this->getConfigBuilder()->getFormConfig();
$config2 = $this->getConfigBuilder()->getFormConfig();
$this->assertSame($config1->getFormProcessor(), $config2->getFormProcessor());
}
public function testSetMethodAllowsGet()
{
$this->getConfigBuilder()->setMethod('GET');
}
public function testSetMethodAllowsPost()
{
$this->getConfigBuilder()->setMethod('POST');
}
public function testSetMethodAllowsPut()
{
$this->getConfigBuilder()->setMethod('PUT');
}
public function testSetMethodAllowsDelete()
{
$this->getConfigBuilder()->setMethod('DELETE');
}
public function testSetMethodAllowsPatch()
{
$this->getConfigBuilder()->setMethod('PATCH');
}
/**
* @expectedException \Symfony\Component\Form\Exception\FormException
*/
public function testSetMethodDoesNotAllowOtherValues()
{
$this->getConfigBuilder()->setMethod('foo');
}
private function getConfigBuilder($name = 'name')
{
$dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
return new FormConfigBuilder($name, null, $dispatcher);
}
}

View File

@ -0,0 +1,219 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests;
use Symfony\Component\Form\NativeFormProcessor;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NativeFormProcessorTest extends AbstractFormProcessorTest
{
private static $serverBackup;
public static function setUpBeforeClass()
{
self::$serverBackup = $_SERVER;
}
protected function setUp()
{
parent::setUp();
$_GET = array();
$_POST = array();
$_FILES = array();
$_SERVER = array(
// PHPUnit needs this entry
'SCRIPT_NAME' => self::$serverBackup['SCRIPT_NAME'],
);
}
protected function tearDown()
{
parent::tearDown();
$_GET = array();
$_POST = array();
$_FILES = array();
$_SERVER = self::$serverBackup;
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testRequestShouldBeNull()
{
$this->processor->processForm($this->getMockForm('name', 'GET'), 'request');
}
public function testMethodOverrideHeaderTakesPrecedenceIfPost()
{
$form = $this->getMockForm('param1', 'PUT');
$this->setRequestData('POST', array(
'param1' => 'DATA',
));
$_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'PUT';
$form->expects($this->once())
->method('bind')
->with('DATA');
$this->processor->processForm($form, $this->request);
}
public function testConvertEmptyUploadedFilesToNull()
{
$form = $this->getMockForm('param1', 'POST', false);
$this->setRequestData('POST', array(), array('param1' => array(
'name' => '',
'type' => '',
'tmp_name' => '',
'error' => UPLOAD_ERR_NO_FILE,
'size' => 0
)));
$form->expects($this->once())
->method('bind')
->with($this->identicalTo(null));
$this->processor->processForm($form, $this->request);
}
public function testFixBuggyFilesArray()
{
$form = $this->getMockForm('param1', 'POST', false);
$this->setRequestData('POST', array(), array('param1' => array(
'name' => array(
'field' => 'upload.txt',
),
'type' => array(
'field' => 'text/plain',
),
'tmp_name' => array(
'field' => 'owfdskjasdfsa',
),
'error' => array(
'field' => UPLOAD_ERR_OK,
),
'size' => array(
'field' => 100,
),
)));
$form->expects($this->once())
->method('bind')
->with(array(
'field' => array(
'name' => 'upload.txt',
'type' => 'text/plain',
'tmp_name' => 'owfdskjasdfsa',
'error' => UPLOAD_ERR_OK,
'size' => 100,
),
));
$this->processor->processForm($form, $this->request);
}
public function testFixBuggyNestedFilesArray()
{
$form = $this->getMockForm('param1', 'POST');
$this->setRequestData('POST', array(), array('param1' => array(
'name' => array(
'field' => array('subfield' => 'upload.txt'),
),
'type' => array(
'field' => array('subfield' => 'text/plain'),
),
'tmp_name' => array(
'field' => array('subfield' => 'owfdskjasdfsa'),
),
'error' => array(
'field' => array('subfield' => UPLOAD_ERR_OK),
),
'size' => array(
'field' => array('subfield' => 100),
),
)));
$form->expects($this->once())
->method('bind')
->with(array(
'field' => array(
'subfield' => array(
'name' => 'upload.txt',
'type' => 'text/plain',
'tmp_name' => 'owfdskjasdfsa',
'error' => UPLOAD_ERR_OK,
'size' => 100,
),
),
));
$this->processor->processForm($form, $this->request);
}
public function testMethodOverrideHeaderIgnoredIfNotPost()
{
$form = $this->getMockForm('param1', 'POST');
$this->setRequestData('GET', array(
'param1' => 'DATA',
));
$_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'PUT';
$form->expects($this->never())
->method('bind');
$this->processor->processForm($form, $this->request);
}
protected function setRequestData($method, $data, $files = array())
{
if ('GET' === $method) {
$_GET = $data;
$_FILES = array();
} else {
$_POST = $data;
$_FILES = $files;
}
$_SERVER = array(
'REQUEST_METHOD' => $method,
// PHPUnit needs this entry
'SCRIPT_NAME' => self::$serverBackup['SCRIPT_NAME'],
);
}
protected function getFormProcessor()
{
return new NativeFormProcessor();
}
protected function getMockFile()
{
return array(
'name' => 'upload.txt',
'type' => 'text/plain',
'tmp_name' => 'owfdskjasdfsa',
'error' => UPLOAD_ERR_OK,
'size' => 100,
);
}
}

View File

@ -275,12 +275,9 @@ class SimpleFormTest extends AbstractFormTest
$this->assertTrue($form->isValid());
}
/**
* @expectedException \LogicException
*/
public function testNotValidIfNotBound()
{
$this->form->isValid();
$this->assertFalse($this->form->isValid());
}
public function testNotValidIfErrors()
@ -845,6 +842,21 @@ class SimpleFormTest extends AbstractFormTest
$parent->bind('not-an-array');
}
public function testProcessForwardsToFormProcessor()
{
$processor = $this->getMock('Symfony\Component\Form\FormProcessorInterface');
$form = $this->getBuilder()
->setFormProcessor($processor)
->getForm();
$processor->expects($this->once())
->method('processForm')
->with($this->identicalTo($form), 'REQUEST');
$this->assertSame($form, $form->process('REQUEST'));
}
protected function createForm()
{
return $this->getBuilder()->getForm();