From 0ea75dbf48821763b868bbd7f8b4daaf31a9378c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sun, 30 Dec 2012 21:48:21 +0100 Subject: [PATCH 1/5] [Form] Improved FormRenderer::renderBlock() to be usable outside of form blocks --- src/Symfony/Component/Form/FormRenderer.php | 32 ++++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Form/FormRenderer.php b/src/Symfony/Component/Form/FormRenderer.php index 3d6cfa9792..09b830b43e 100644 --- a/src/Symfony/Component/Form/FormRenderer.php +++ b/src/Symfony/Component/Form/FormRenderer.php @@ -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; From 81f8c67566a65257ed61ef88e52eb25e4b4ca8ba Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sun, 30 Dec 2012 16:38:36 +0100 Subject: [PATCH 2/5] [Form] Implemented form processors --- src/Symfony/Bridge/Twig/CHANGELOG.md | 9 + .../Bridge/Twig/Extension/FormExtension.php | 4 +- .../Bridge/Twig/Node/FormEnctypeNode.php | 29 ++ .../Bridge/Twig/Node/RenderBlockNode.php | 42 +++ .../views/Form/form_div_layout.html.twig | 32 ++ .../Extension/FormExtensionDivLayoutTest.php | 15 + .../FormExtensionTableLayoutTest.php | 15 + .../Bundle/FrameworkBundle/CHANGELOG.md | 7 +- .../Resources/views/Form/form.html.php | 3 + .../Resources/views/Form/form_end.html.php | 4 + .../Resources/views/Form/form_start.html.php | 6 + .../Templating/Helper/FormHelper.php | 76 ++++- .../Helper/FormHelperDivLayoutTest.php | 15 + .../Helper/FormHelperTableLayoutTest.php | 15 + src/Symfony/Component/Form/Button.php | 13 + src/Symfony/Component/Form/ButtonBuilder.php | 66 +++++ src/Symfony/Component/Form/CHANGELOG.md | 3 + .../Form/Extension/Core/Type/FormType.php | 8 + .../EventListener/BindRequestListener.php | 5 + .../HttpFoundation/RequestFormProcessor.php | 80 +++++ .../Type/FormTypeHttpFoundationExtension.php | 8 + src/Symfony/Component/Form/Form.php | 12 +- .../Component/Form/FormConfigBuilder.php | 128 +++++++- .../Form/FormConfigBuilderInterface.php | 25 ++ .../Component/Form/FormConfigInterface.php | 19 ++ src/Symfony/Component/Form/FormInterface.php | 15 + .../Component/Form/FormProcessorInterface.php | 28 ++ .../Component/Form/NativeFormProcessor.php | 194 ++++++++++++ .../Form/Test/DeprecationErrorHandler.php | 9 + .../Form/Tests/AbstractDivLayoutTest.php | 90 ++++++ .../Form/Tests/AbstractFormProcessorTest.php | 280 ++++++++++++++++++ .../Form/Tests/AbstractLayoutTest.php | 85 +++++- .../Form/Tests/AbstractTableLayoutTest.php | 106 +++++++ .../Component/Form/Tests/CompoundFormTest.php | 33 ++- .../EventListener/BindRequestListenerTest.php | 17 +- .../RequestFormProcessorTest.php | 54 ++++ .../Component/Form/Tests/FormConfigTest.php | 58 +++- .../Form/Tests/FormIntegrationTestCase.php | 9 + .../Form/Tests/NativeFormProcessorTest.php | 219 ++++++++++++++ .../Component/Form/Tests/SimpleFormTest.php | 20 +- 40 files changed, 1809 insertions(+), 47 deletions(-) create mode 100644 src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php create mode 100644 src/Symfony/Bridge/Twig/Node/RenderBlockNode.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form.html.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_end.html.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php create mode 100644 src/Symfony/Component/Form/Extension/HttpFoundation/RequestFormProcessor.php create mode 100644 src/Symfony/Component/Form/FormProcessorInterface.php create mode 100644 src/Symfony/Component/Form/NativeFormProcessor.php create mode 100644 src/Symfony/Component/Form/Tests/AbstractFormProcessorTest.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/HttpFoundation/RequestFormProcessorTest.php create mode 100644 src/Symfony/Component/Form/Tests/NativeFormProcessorTest.php diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 343c7743a3..0ed2a2ddee 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +2.3.0 +----- + + * [BC BREAK] restricted the `render` tag to only accept URIs as reference (the signature changed) + * added a render function to render a request + * the `app` global variable is now injected even when using the twig service directly. + * added helpers form(), form_start() and form_end() + * deprecated form_enctype() in favor of form_start() + 2.2.0 ----- diff --git a/src/Symfony/Bridge/Twig/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 0609fb675d..de3d0bc40c 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -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'), ); } diff --git a/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php b/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php new file mode 100644 index 0000000000..73c1b7748e --- /dev/null +++ b/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php @@ -0,0 +1,29 @@ + + * + * 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 + * + * @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"); + $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)'); + } +} diff --git a/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php b/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php new file mode 100644 index 0000000000..822a27279f --- /dev/null +++ b/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php @@ -0,0 +1,42 @@ + + * + * 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 + */ +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(')'); + } +} 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 188896a76b..453c29c65b 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 @@ -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 %} +
+ {% if form_method != method %} + + {% endif %} +{% endspaceless %} +{% endblock form_start %} + +{% block form_end %} +{% spaceless %} + {% if not render_rest is defined or render_rest %} + {{ form_rest(form) }} + {% endif %} +
+{% endspaceless %} +{% endblock form_end %} + {% block form_enctype %} {% spaceless %} {% if multipart %}enctype="multipart/form-data"{% endif %} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php index b525e77c8c..c5c134bc1c 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php @@ -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); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php index da0d846e63..99a782178a 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php @@ -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); diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index b24e4ba04a..48dd3ee5e8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form.html.php new file mode 100644 index 0000000000..fb789faff7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form.html.php @@ -0,0 +1,3 @@ +start($form) ?> + widget($form) ?> +end($form) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_end.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_end.html.php new file mode 100644 index 0000000000..fe6843905c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_end.html.php @@ -0,0 +1,4 @@ + +rest($form) ?> + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php new file mode 100644 index 0000000000..9c3af35ffb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php @@ -0,0 +1,6 @@ + + +
$v) { printf(' %s="%s"', $view->escape($k), $view->escape($v)); } ?> enctype="multipart/form-data"> + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php index 9ebd882a06..6329653a47 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php @@ -57,19 +57,87 @@ class FormHelper extends Helper $this->renderer->setTheme($view, $themes); } + /** + * Renders the HTML for a form. + * + * Example usage: + * + * form($form) ?> + * + * You can pass options during the call: + * + * form($form, array('attr' => array('class' => 'foo'))) ?> + * + * 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: + * + * 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: + * + * 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: * - * enctype() ?>> + * 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) { + 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 +146,13 @@ class FormHelper extends Helper * * Example usage: * - * widget() ?> + * widget($form) ?> * * You can pass options during the call: * - * widget(array('attr' => array('class' => 'foo'))) ?> + * widget($form, array('attr' => array('class' => 'foo'))) ?> * - * widget(array('separator' => '+++++')) ?> + * widget($form, array('separator' => '+++++')) ?> * * @param FormView $view The view for which to render the widget * @param array $variables Additional variables passed to the template diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php index 98c82d58b6..9a960e3bea 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php @@ -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); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php index 5aaea306bd..96c56b6506 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php @@ -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); diff --git a/src/Symfony/Component/Form/Button.php b/src/Symfony/Component/Form/Button.php index 23c4775530..77af61b8d8 100644 --- a/src/Symfony/Component/Form/Button.php +++ b/src/Symfony/Component/Form/Button.php @@ -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. * diff --git a/src/Symfony/Component/Form/ButtonBuilder.php b/src/Symfony/Component/Form/ButtonBuilder.php index 9e61cbf403..fb77ef00ff 100644 --- a/src/Symfony/Component/Form/ButtonBuilder.php +++ b/src/Symfony/Component/Form/ButtonBuilder.php @@ -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. * diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index b315af5cbc..40976fa8d0 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index bd01f8f040..e51dba870e 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -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( diff --git a/src/Symfony/Component/Form/Extension/HttpFoundation/EventListener/BindRequestListener.php b/src/Symfony/Component/Form/Extension/HttpFoundation/EventListener/BindRequestListener.php index b4d0ed7b13..1eb4e6f297 100644 --- a/src/Symfony/Component/Form/Extension/HttpFoundation/EventListener/BindRequestListener.php +++ b/src/Symfony/Component/Form/Extension/HttpFoundation/EventListener/BindRequestListener.php @@ -19,6 +19,9 @@ use Symfony\Component\HttpFoundation\Request; /** * @author Bernhard Schussek + * + * @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,8 @@ class BindRequestListener implements EventSubscriberInterface return; } + 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; diff --git a/src/Symfony/Component/Form/Extension/HttpFoundation/RequestFormProcessor.php b/src/Symfony/Component/Form/Extension/HttpFoundation/RequestFormProcessor.php new file mode 100644 index 0000000000..c2f0b4de2c --- /dev/null +++ b/src/Symfony/Component/Form/Extension/HttpFoundation/RequestFormProcessor.php @@ -0,0 +1,80 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Form/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php b/src/Symfony/Component/Form/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php index 936f58efdd..28e6e7c816 100644 --- a/src/Symfony/Component/Form/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php +++ b/src/Symfony/Component/Form/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php @@ -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); } /** diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 3471979411..3b95817be0 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -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) { diff --git a/src/Symfony/Component/Form/FormConfigBuilder.php b/src/Symfony/Component/Form/FormConfigBuilder.php index c52b4169e8..694b94555e 100644 --- a/src/Symfony/Component/Form/FormConfigBuilder.php +++ b/src/Symfony/Component/Form/FormConfigBuilder.php @@ -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; } diff --git a/src/Symfony/Component/Form/FormConfigBuilderInterface.php b/src/Symfony/Component/Form/FormConfigBuilderInterface.php index c0d8c2b1f9..34b217e623 100644 --- a/src/Symfony/Component/Form/FormConfigBuilderInterface.php +++ b/src/Symfony/Component/Form/FormConfigBuilderInterface.php @@ -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. * diff --git a/src/Symfony/Component/Form/FormConfigInterface.php b/src/Symfony/Component/Form/FormConfigInterface.php index 002b54cfac..e00ce27f43 100644 --- a/src/Symfony/Component/Form/FormConfigInterface.php +++ b/src/Symfony/Component/Form/FormConfigInterface.php @@ -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. * diff --git a/src/Symfony/Component/Form/FormInterface.php b/src/Symfony/Component/Form/FormInterface.php index 3e8fe525ec..05d95b2282 100644 --- a/src/Symfony/Component/Form/FormInterface.php +++ b/src/Symfony/Component/Form/FormInterface.php @@ -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. * diff --git a/src/Symfony/Component/Form/FormProcessorInterface.php b/src/Symfony/Component/Form/FormProcessorInterface.php new file mode 100644 index 0000000000..3634524b8d --- /dev/null +++ b/src/Symfony/Component/Form/FormProcessorInterface.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; + +/** + * Binds forms from requests if they were submitted. + * + * @author Bernhard Schussek + */ +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); +} diff --git a/src/Symfony/Component/Form/NativeFormProcessor.php b/src/Symfony/Component/Form/NativeFormProcessor.php new file mode 100644 index 0000000000..7494c84eaf --- /dev/null +++ b/src/Symfony/Component/Form/NativeFormProcessor.php @@ -0,0 +1,194 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Form/Test/DeprecationErrorHandler.php b/src/Symfony/Component/Form/Test/DeprecationErrorHandler.php index 21b3a6e6b0..a2cce590be 100644 --- a/src/Symfony/Component/Form/Test/DeprecationErrorHandler.php +++ b/src/Symfony/Component/Form/Test/DeprecationErrorHandler.php @@ -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(); + } } diff --git a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php index 391e81173e..23cecaee94 100644 --- a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php @@ -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('' . $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('', $html); + } } diff --git a/src/Symfony/Component/Form/Tests/AbstractFormProcessorTest.php b/src/Symfony/Component/Form/Tests/AbstractFormProcessorTest.php new file mode 100644 index 0000000000..40270b2c0e --- /dev/null +++ b/src/Symfony/Component/Form/Tests/AbstractFormProcessorTest.php @@ -0,0 +1,280 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 1dda383253..176fb17962 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -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('
', $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 + [./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('
', $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('', $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('', $html); + } } diff --git a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php index 28fdba2dda..5c91195169 100644 --- a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php @@ -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 + // tag. + $this->assertMatchesXpath('' . $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('', $html); + } } diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php index 06e9b4d19e..1d52075d6c 100644 --- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php +++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php @@ -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()); diff --git a/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/EventListener/BindRequestListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/EventListener/BindRequestListenerTest.php index bfb9afa0a7..0c534fd0be 100644 --- a/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/EventListener/BindRequestListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/EventListener/BindRequestListenerTest.php @@ -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()); } diff --git a/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/RequestFormProcessorTest.php b/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/RequestFormProcessorTest.php new file mode 100644 index 0000000000..95e522c474 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/RequestFormProcessorTest.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\HttpFoundation; + +use Symfony\Component\Form\Extension\HttpFoundation\RequestFormProcessor; +use Symfony\Component\Form\Tests\AbstractFormProcessorTest; +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Bernhard Schussek + */ +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(); + } +} diff --git a/src/Symfony/Component/Form/Tests/FormConfigTest.php b/src/Symfony/Component/Form/Tests/FormConfigTest.php index a7626f86a6..4337a2ab68 100644 --- a/src/Symfony/Component/Form/Tests/FormConfigTest.php +++ b/src/Symfony/Component/Form/Tests/FormConfigTest.php @@ -12,12 +12,11 @@ namespace Symfony\Component\Form\Tests; use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\FormConfigBuilder; /** * @author Bernhard Schussek */ -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); + } } diff --git a/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php b/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php index 536ff4c824..e72ec4723b 100644 --- a/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php +++ b/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php @@ -32,6 +32,15 @@ abstract class FormIntegrationTestCase extends \PHPUnit_Framework_TestCase $this->factory = Forms::createFormFactoryBuilder() ->addExtensions($this->getExtensions()) ->getFormFactory(); + + set_error_handler(array('Symfony\Component\Form\Test\DeprecationErrorHandler', 'handle')); + } + + protected function tearDown() + { + $this->factory = null; + + restore_error_handler(); } protected function getExtensions() diff --git a/src/Symfony/Component/Form/Tests/NativeFormProcessorTest.php b/src/Symfony/Component/Form/Tests/NativeFormProcessorTest.php new file mode 100644 index 0000000000..4e90e5276c --- /dev/null +++ b/src/Symfony/Component/Form/Tests/NativeFormProcessorTest.php @@ -0,0 +1,219 @@ + + * + * 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 + */ +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, + ); + } +} diff --git a/src/Symfony/Component/Form/Tests/SimpleFormTest.php b/src/Symfony/Component/Form/Tests/SimpleFormTest.php index 6d69f259dc..9493b85ce1 100644 --- a/src/Symfony/Component/Form/Tests/SimpleFormTest.php +++ b/src/Symfony/Component/Form/Tests/SimpleFormTest.php @@ -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(); From 01b71a47ead348491b6ea052be48a9c6b5c0ec47 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 15 Apr 2013 11:15:47 +0200 Subject: [PATCH 3/5] [Form] Removed trigger_error() for deprecations as of 3.0 --- src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php | 4 +++- .../FrameworkBundle/Templating/Helper/FormHelper.php | 3 ++- .../HttpFoundation/EventListener/BindRequestListener.php | 3 ++- .../Component/Form/Tests/FormIntegrationTestCase.php | 9 --------- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php b/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php index 73c1b7748e..93bce1b9e6 100644 --- a/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php +++ b/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php @@ -24,6 +24,8 @@ class FormEnctypeNode extends SearchAndRenderBlockNode parent::compile($compiler); $compiler->raw(";\n"); - $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)'); + + // 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)'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php index 6329653a47..29220af5ab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php @@ -136,7 +136,8 @@ class FormHelper extends Helper */ public function enctype(FormView $view) { - 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); + // 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'); } diff --git a/src/Symfony/Component/Form/Extension/HttpFoundation/EventListener/BindRequestListener.php b/src/Symfony/Component/Form/Extension/HttpFoundation/EventListener/BindRequestListener.php index 1eb4e6f297..3b4359ed5c 100644 --- a/src/Symfony/Component/Form/Extension/HttpFoundation/EventListener/BindRequestListener.php +++ b/src/Symfony/Component/Form/Extension/HttpFoundation/EventListener/BindRequestListener.php @@ -43,7 +43,8 @@ class BindRequestListener implements EventSubscriberInterface return; } - 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); + // 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; diff --git a/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php b/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php index e72ec4723b..536ff4c824 100644 --- a/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php +++ b/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php @@ -32,15 +32,6 @@ abstract class FormIntegrationTestCase extends \PHPUnit_Framework_TestCase $this->factory = Forms::createFormFactoryBuilder() ->addExtensions($this->getExtensions()) ->getFormFactory(); - - set_error_handler(array('Symfony\Component\Form\Test\DeprecationErrorHandler', 'handle')); - } - - protected function tearDown() - { - $this->factory = null; - - restore_error_handler(); } protected function getExtensions() From 68f360c92f9135fa95617f60e9c12d4252433f73 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 17 Apr 2013 18:47:21 +0200 Subject: [PATCH 4/5] [Form] Moved upgrade nodes to UPGRADE-3.0 --- UPGRADE-2.3.md | 2 +- UPGRADE-3.0.md | 141 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/UPGRADE-2.3.md b/UPGRADE-2.3.md index 5e83e86ac2..4399e88bad 100644 --- a/UPGRADE-2.3.md +++ b/UPGRADE-2.3.md @@ -1,4 +1,4 @@ -UPGRADE FROM 2.2 to 2.3 +UPGRADE FROM 2.2 to 2.3 ======================= ### Form diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index 50120a3611..42b0542845 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -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: + + ``` +
enctype($form) ?>> + ... + + ``` + + After: + + ``` + start($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: + + ``` + start($form, array('method' => 'GET', 'action' => 'http://example.com')) ?> + ... + 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: + + ``` +
+ ... + + ``` + + 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. From 11fee0603525f8ffce34dac197b50f5c233f0990 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 18 Apr 2013 12:18:55 +0200 Subject: [PATCH 5/5] [TwigBridge] Removed duplicate entries from the CHANGELOG --- src/Symfony/Bridge/Twig/CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 0ed2a2ddee..ad22216e40 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -4,9 +4,6 @@ CHANGELOG 2.3.0 ----- - * [BC BREAK] restricted the `render` tag to only accept URIs as reference (the signature changed) - * added a render function to render a request - * the `app` global variable is now injected even when using the twig service directly. * added helpers form(), form_start() and form_end() * deprecated form_enctype() in favor of form_start()