From 77e9ff973f233d290f82c5214982ced83988341c Mon Sep 17 00:00:00 2001 From: Christian Raue Date: Tue, 29 Jul 2014 13:04:54 +0200 Subject: [PATCH 1/2] extracted methods for adding/removing route parameters into a separate class --- .../CraueFormFlowExtension.php | 1 + Resources/config/twig.xml | 3 + Resources/config/util.xml | 18 +++++ Tests/Util/FormFlowUtilTest.php | 78 +++++++++++++++++++ Twig/Extension/FormFlowExtension.php | 24 +++--- Util/FormFlowUtil.php | 45 +++++++++++ 6 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 Resources/config/util.xml create mode 100644 Tests/Util/FormFlowUtilTest.php create mode 100644 Util/FormFlowUtil.php diff --git a/DependencyInjection/CraueFormFlowExtension.php b/DependencyInjection/CraueFormFlowExtension.php index ac9fd427..d9920a7a 100644 --- a/DependencyInjection/CraueFormFlowExtension.php +++ b/DependencyInjection/CraueFormFlowExtension.php @@ -23,6 +23,7 @@ public function load(array $config, ContainerBuilder $container) { $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('form_flow.xml'); $loader->load('twig.xml'); + $loader->load('util.xml'); } } diff --git a/Resources/config/twig.xml b/Resources/config/twig.xml index 1509e859..19f9b444 100644 --- a/Resources/config/twig.xml +++ b/Resources/config/twig.xml @@ -15,6 +15,9 @@ + + + diff --git a/Resources/config/util.xml b/Resources/config/util.xml new file mode 100644 index 00000000..74290caf --- /dev/null +++ b/Resources/config/util.xml @@ -0,0 +1,18 @@ + + + + + Craue\FormFlowBundle\Util\FormFlowUtil + + + + + + diff --git a/Tests/Util/FormFlowUtilTest.php b/Tests/Util/FormFlowUtilTest.php new file mode 100644 index 00000000..5812689a --- /dev/null +++ b/Tests/Util/FormFlowUtilTest.php @@ -0,0 +1,78 @@ + + * @copyright 2011-2014 Christian Raue + * @license http://opensource.org/licenses/mit-license.php MIT License + */ +class FormFlowUtilTest extends \PHPUnit_Framework_TestCase { + + /** + * @var FormFlowUtil + */ + protected $util; + + /** + * {@inheritDoc} + */ + protected function setUp() { + $this->util = new FormFlowUtil(); + } + + public function testAddRouteParameters() { + $flow = $this->getFlowWithMockedMethods(array('getName', 'loadStepsConfig')); + + $flow + ->expects($this->once()) + ->method('loadStepsConfig') + ->will($this->returnValue(array( + array(), + array(), + ))) + ; + + $instanceId = 'xyz'; + $flow->setInstanceId($instanceId); + + $flow->nextStep(); + + $actualParameters = $this->util->addRouteParameters(array('key' => 'value'), $flow); + + $this->assertEquals(array('key' => 'value', 'instance' => $instanceId, 'step' => 1), $actualParameters); + } + + public function testAddRouteParameters_explicitStepNumber() { + $flow = $this->getFlowWithMockedMethods(array('getName')); + + $instanceId = 'xyz'; + $flow->setInstanceId($instanceId); + + $actualParameters = $this->util->addRouteParameters(array('key' => 'value'), $flow, 5); + + $this->assertEquals(array('key' => 'value', 'instance' => $instanceId, 'step' => 5), $actualParameters); + } + + public function testRemoveRouteParameters() { + $flow = $this->getFlowWithMockedMethods(array('getName')); + + $actualParameters = $this->util->removeRouteParameters(array('key' => 'value', 'instance' => 'xyz', 'step' => 2), $flow); + + $this->assertEquals(array('key' => 'value'), $actualParameters); + } + + /** + * @param string[] $methodNames Names of methods to be mocked. + * @return PHPUnit_Framework_MockObject_MockObject|FormFlow + */ + protected function getFlowWithMockedMethods(array $methodNames) { + return $this->getMock('\Craue\FormFlowBundle\Form\FormFlow', $methodNames); + } + +} diff --git a/Twig/Extension/FormFlowExtension.php b/Twig/Extension/FormFlowExtension.php index 2b215df9..933cc39e 100644 --- a/Twig/Extension/FormFlowExtension.php +++ b/Twig/Extension/FormFlowExtension.php @@ -3,6 +3,7 @@ namespace Craue\FormFlowBundle\Twig\Extension; use Craue\FormFlowBundle\Form\FormFlow; +use Craue\FormFlowBundle\Util\FormFlowUtil; /** * Twig extension for form flows. @@ -13,6 +14,15 @@ */ class FormFlowExtension extends \Twig_Extension { + /** + * @var FormFlowUtil + */ + protected $formFlowUtil; + + public function setFormFlowUtil(FormFlowUtil $formFlowUtil) { + $this->formFlowUtil = $formFlowUtil; + } + /** * {@inheritDoc} */ @@ -42,30 +52,24 @@ public function getFunctions() { } /** - * Adds parameters for dynamic step navigation. + * Adds route parameters for dynamic step navigation. * @param array $parameters Current route parameters. * @param FormFlow $flow The flow involved. * @param integer $stepNumber Number of the step the link will be generated for. * @return array Route parameters plus instance and step parameter. */ public function addDynamicStepNavigationParameters(array $parameters, FormFlow $flow, $stepNumber) { - $parameters[$flow->getDynamicStepNavigationInstanceParameter()] = $flow->getInstanceId(); - $parameters[$flow->getDynamicStepNavigationStepParameter()] = $stepNumber; - - return $parameters; + return $this->formFlowUtil->addRouteParameters($parameters, $flow, $stepNumber); } /** - * Removes parameters for dynamic step navigation. + * Removes route parameters for dynamic step navigation. * @param array $parameters Current route parameters. * @param FormFlow $flow The flow involved. * @return array Route parameters without instance and step parameter. */ public function removeDynamicStepNavigationParameters(array $parameters, FormFlow $flow) { - unset($parameters[$flow->getDynamicStepNavigationInstanceParameter()]); - unset($parameters[$flow->getDynamicStepNavigationStepParameter()]); - - return $parameters; + return $this->formFlowUtil->removeRouteParameters($parameters, $flow); } /** diff --git a/Util/FormFlowUtil.php b/Util/FormFlowUtil.php new file mode 100644 index 00000000..00b965d8 --- /dev/null +++ b/Util/FormFlowUtil.php @@ -0,0 +1,45 @@ + + * @copyright 2011-2014 Christian Raue + * @license http://opensource.org/licenses/mit-license.php MIT License + */ +class FormFlowUtil { + + /** + * Adds route parameters for dynamic step navigation. + * @param array $parameters Current route parameters. + * @param FormFlow $flow The flow involved. + * @param integer|null $stepNumber Number of the step the link will be generated for. If {@code null}, the {@code $flow}'s current step number will be used. + * @return array Route parameters plus instance and step parameter. + */ + public function addRouteParameters(array $parameters, FormFlow $flow, $stepNumber = null) { + if ($stepNumber === null) { + $stepNumber = $flow->getCurrentStepNumber(); + } + + $parameters[$flow->getDynamicStepNavigationInstanceParameter()] = $flow->getInstanceId(); + $parameters[$flow->getDynamicStepNavigationStepParameter()] = $stepNumber; + + return $parameters; + } + + /** + * Removes route parameters for dynamic step navigation. + * @param array $parameters Current route parameters. + * @param FormFlow $flow The flow involved. + * @return array Route parameters without instance and step parameter. + */ + public function removeRouteParameters(array $parameters, FormFlow $flow) { + unset($parameters[$flow->getDynamicStepNavigationInstanceParameter()]); + unset($parameters[$flow->getDynamicStepNavigationStepParameter()]); + + return $parameters; + } + +} From 12d0d7b5a7188c80b8bc18d689f3623caf9ec4a3 Mon Sep 17 00:00:00 2001 From: Christian Raue Date: Tue, 29 Jul 2014 13:05:37 +0200 Subject: [PATCH 2/2] added support for the "redirect after submit" pattern --- CHANGELOG.md | 2 + Form/FormFlow.php | 41 +++++++- Form/FormFlowInterface.php | 5 + README.md | 45 ++++++++- Tests/CreateTopicFlowTest.php | 97 +++++++++++++++++++ Tests/Form/FormFlowTest.php | 11 +++ .../Controller/FormFlowController.php | 26 ++++- .../Resources/views/layout_flow.html.twig | 7 +- 8 files changed, 225 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61460fa5..c36ce68a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - [#125]: added generic form options to simplify passing options to all steps - [#126]: allow custom classes on buttons - [#133]+[#134]: added Farsi translation +- [#142]: added support for the "redirect after submit" pattern - [#146]: handling of file uploads [#98]: https://github.com/craue/CraueFormFlowBundle/issues/98 @@ -28,6 +29,7 @@ [#126]: https://github.com/craue/CraueFormFlowBundle/issues/126 [#133]: https://github.com/craue/CraueFormFlowBundle/issues/133 [#134]: https://github.com/craue/CraueFormFlowBundle/issues/134 +[#142]: https://github.com/craue/CraueFormFlowBundle/issues/142 [#143]: https://github.com/craue/CraueFormFlowBundle/issues/143 [#146]: https://github.com/craue/CraueFormFlowBundle/issues/146 diff --git a/Form/FormFlow.php b/Form/FormFlow.php index 8ba13355..5058d698 100644 --- a/Form/FormFlow.php +++ b/Form/FormFlow.php @@ -71,6 +71,11 @@ abstract class FormFlow implements FormFlowInterface { */ protected $handleFileUploadsTempDir = null; + /** + * @var boolean + */ + protected $allowRedirectAfterSubmit = false; + /** * @var string */ @@ -368,6 +373,17 @@ public function getHandleFileUploadsTempDir() { return $this->handleFileUploadsTempDir; } + public function setAllowRedirectAfterSubmit($allowRedirectAfterSubmit) { + $this->allowRedirectAfterSubmit = (boolean) $allowRedirectAfterSubmit; + } + + /** + * {@inheritDoc} + */ + public function isAllowRedirectAfterSubmit() { + return $this->allowRedirectAfterSubmit; + } + public function setDynamicStepNavigationInstanceParameter($dynamicStepNavigationInstanceParameter) { $this->dynamicStepNavigationInstanceParameter = $dynamicStepNavigationInstanceParameter; } @@ -500,7 +516,7 @@ protected function getRequestedStepNumber() { case 'POST': return intval($request->request->get($this->getFormStepKey(), $defaultStepNumber)); case 'GET': - return $this->allowDynamicStepNavigation ? + return $this->allowDynamicStepNavigation || $this->allowRedirectAfterSubmit ? intval($request->get($this->dynamicStepNavigationStepParameter, $defaultStepNumber)) : $defaultStepNumber; } @@ -581,7 +597,7 @@ protected function determineInstanceId() { $instanceId = null; - if ($this->allowDynamicStepNavigation) { + if ($this->allowDynamicStepNavigation || $this->allowRedirectAfterSubmit) { $instanceId = $request->get($this->getDynamicStepNavigationInstanceParameter()); } @@ -595,7 +611,7 @@ protected function determineInstanceId() { protected function bindFlow() { $reset = false; - if (!$this->allowDynamicStepNavigation && $this->getRequest()->isMethod('GET')) { + if (!$this->allowDynamicStepNavigation && !$this->allowRedirectAfterSubmit && $this->getRequest()->isMethod('GET')) { $reset = true; } @@ -853,6 +869,25 @@ public function isValid(FormInterface $form) { return false; } + /** + * @param FormInterface $submittedForm + * @return boolean If a redirection should be performed. + */ + public function redirectAfterSubmit(FormInterface $submittedForm) { + if ($this->allowRedirectAfterSubmit && in_array($this->getRequest()->getMethod(), array('POST', 'PUT'))) { + switch ($this->getRequestedTransition()) { + case self::TRANSITION_BACK: + case self::TRANSITION_RESET: + return true; + default: + // redirect after submit only if there are no errors for the submitted form + return $submittedForm->isValid(); + } + } + + return false; + } + /** * Creates the form for the given step number. * @param integer $stepNumber diff --git a/Form/FormFlowInterface.php b/Form/FormFlowInterface.php index 6359356a..7888e7b9 100644 --- a/Form/FormFlowInterface.php +++ b/Form/FormFlowInterface.php @@ -66,6 +66,11 @@ function isHandleFileUploads(); */ function getHandleFileUploadsTempDir(); + /** + * @return boolean + */ + function isAllowRedirectAfterSubmit(); + /** * @return string */ diff --git a/README.md b/README.md index 58828f42..d67e6c70 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ Features: - skipping of steps - different validation group for each step - handling of file uploads -- dynamic step navigation +- dynamic step navigation (optional) +- redirect after submit (a.k.a. "Post/Redirect/Get", optional) A live demo showcasing these features is available at http://craue.de/sf2playground/en/CraueFormFlow/. @@ -571,6 +572,48 @@ class CreateVehicleFlow extends FormFlow { } ``` +## Enabling redirect after submit + +This feature will allow performing a redirect after submitting a step to load the page containing the next step using a GET request. +To enable it you could extend the flow class mentioned in the example above as follows: + +```php +// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php +class CreateVehicleFlow extends FormFlow { + + protected $allowRedirectAfterSubmit = true; + + // ... + +} +``` + +But you still have to perform the redirect yourself, so update your action like this: + +```php +// in src/MyCompany/MyBundle/Controller/VehicleController.php +public function createVehicleAction() { + // ... + $flow->bind($formData); + $form = $submittedForm = $flow->createForm(); + if ($flow->isValid($submittedForm)) { + $flow->saveCurrentStepData($submittedForm); + // ... + } + + if ($flow->redirectAfterSubmit($submittedForm)) { + $request = $this->getRequest(); + $params = $this->get('craue_formflow_util')->addRouteParameters(array_merge($request->query->all(), + $request->attributes->get('_route_params')), $flow); + + return $this->redirect($this->generateUrl($request->attributes->get('_route'), $params)); + } + + // ... + // return ... +} +``` + ## Using events There are some events which you can subscribe to. Using all of them right inside your flow class could look like this: diff --git a/Tests/CreateTopicFlowTest.php b/Tests/CreateTopicFlowTest.php index 094af4d3..de4ad353 100644 --- a/Tests/CreateTopicFlowTest.php +++ b/Tests/CreateTopicFlowTest.php @@ -147,4 +147,101 @@ public function testCreateTopic_dynamicStepNavigation_newFlowInstanceOnGetReques $this->assertCurrentFormData('{"title":null,"description":null,"category":null,"comment":null,"details":null}', $crawler); } + public function testCreateTopic_redirectAfterSubmit() { + $this->client->followRedirects(); + $crawler = $this->client->request('GET', $this->url('_FormFlow_createTopic_redirectAfterSubmit')); + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertCurrentStepNumber(1, $crawler); + $this->assertCurrentFormData('{"title":null,"description":null,"category":null,"comment":null,"details":null}', $crawler); + $this->assertCount(0, $crawler->filter('#step-list a')); + + // reset -> step 1 + $form = $crawler->selectButton('start over')->form(); + $crawler = $this->client->submit($form); + $this->assertCurrentStepNumber(1, $crawler); + $this->assertCurrentFormData('{"title":null,"description":null,"category":null,"comment":null,"details":null}', $crawler); + $this->assertCount(0, $crawler->filter('#step-list a')); + // make sure redirection was effective after clicking "start over" + $this->assertEquals('GET', $this->client->getRequest()->getMethod()); + $this->assertArrayHasKey('instance', $this->client->getRequest()->query->all()); + $this->assertEquals(1, $this->client->getRequest()->query->get('step')); + + // empty title -> step 1 again + $form = $crawler->selectButton('next')->form(); + $crawler = $this->client->submit($form, array( + 'createTopic[title]' => '', + )); + $this->assertCurrentStepNumber(1, $crawler); + $this->assertContainsFormError('This value should not be blank.', $crawler); + $this->assertCurrentFormData('{"title":null,"description":null,"category":null,"comment":null,"details":null}', $crawler); + // make sure query parameters are still added in case of form errors + $this->assertEquals('POST', $this->client->getRequest()->getMethod()); + $this->assertArrayHasKey('instance', $this->client->getRequest()->query->all()); + $this->assertEquals(1, $this->client->getRequest()->query->get('step')); + + // bug report -> step 2 + $form = $crawler->selectButton('next')->form(); + $crawler = $this->client->submit($form, array( + 'createTopic[title]' => 'blah', + 'createTopic[category]' => 'BUG_REPORT', + )); + $this->assertCurrentStepNumber(2, $crawler); + $this->assertCurrentFormData('{"title":"blah","description":null,"category":"BUG_REPORT","comment":null,"details":null}', $crawler); + $this->assertCount(0, $crawler->filter('#step-list a')); + // make sure redirection was effective after clicking "next" + $this->assertEquals('GET', $this->client->getRequest()->getMethod()); + $this->assertArrayHasKey('instance', $this->client->getRequest()->query->all()); + $this->assertEquals(2, $this->client->getRequest()->query->get('step')); + + // comment -> step 3 + $form = $crawler->selectButton('next')->form(); + $crawler = $this->client->submit($form, array( + 'createTopic[comment]' => 'my comment', + )); + $this->assertCurrentStepNumber(3, $crawler); + $this->assertCurrentFormData('{"title":"blah","description":null,"category":"BUG_REPORT","comment":"my comment","details":null}', $crawler); + $this->assertCount(0, $crawler->filter('#step-list a')); + + // empty bug details -> step 3 again + $form = $crawler->selectButton('next')->form(); + $crawler = $this->client->submit($form, array( + 'createTopic[details]' => '', + )); + $this->assertCurrentStepNumber(3, $crawler); + $this->assertContainsFormError('This value should not be blank.', $crawler); + $this->assertCurrentFormData('{"title":"blah","description":null,"category":"BUG_REPORT","comment":"my comment","details":null}', $crawler); + + // bug details -> step 4 + $form = $crawler->selectButton('next')->form(); + $crawler = $this->client->submit($form, array( + 'createTopic[details]' => 'blah blah', + )); + $this->assertCurrentStepNumber(4, $crawler); + $this->assertCurrentFormData('{"title":"blah","description":null,"category":"BUG_REPORT","comment":"my comment","details":"blah blah"}', $crawler); + $this->assertCount(0, $crawler->filter('#step-list a')); + + // back -> step 3 + $form = $crawler->selectButton('back')->form(); + $crawler = $this->client->submit($form); + $this->assertCurrentStepNumber(3, $crawler); + $this->assertCurrentFormData('{"title":"blah","description":null,"category":"BUG_REPORT","comment":"my comment","details":"blah blah"}', $crawler); + $this->assertCount(0, $crawler->filter('#step-list a')); + // make sure redirection was effective after clicking "back" + $this->assertEquals('GET', $this->client->getRequest()->getMethod()); + $this->assertArrayHasKey('instance', $this->client->getRequest()->query->all()); + $this->assertEquals(3, $this->client->getRequest()->query->get('step')); + + // next -> step 4 + $form = $crawler->selectButton('next')->form(); + $crawler = $this->client->submit($form); + $this->assertCurrentStepNumber(4, $crawler); + $this->assertCurrentFormData('{"title":"blah","description":null,"category":"BUG_REPORT","comment":"my comment","details":"blah blah"}', $crawler); + $this->assertCount(0, $crawler->filter('#step-list a')); + + // finish flow + $form = $crawler->selectButton('finish')->form(); + $this->client->submit($form); + $this->assertJsonResponse('{"title":"blah","description":null,"category":"BUG_REPORT","comment":"my comment","details":"blah blah"}'); + } + } diff --git a/Tests/Form/FormFlowTest.php b/Tests/Form/FormFlowTest.php index 73c90ef8..81824809 100644 --- a/Tests/Form/FormFlowTest.php +++ b/Tests/Form/FormFlowTest.php @@ -325,6 +325,17 @@ public function dataSetGetHandleFileUploadsTempDir() { ); } + /** + * @dataProvider dataBooleanSetter + */ + public function testSetIsAllowRedirectAfterSubmit($expectedValue, $allowRedirectAfterSubmit) { + $flow = $this->getMockedFlow(); + + $flow->setAllowRedirectAfterSubmit($allowRedirectAfterSubmit); + + $this->assertEquals($expectedValue, $flow->isAllowRedirectAfterSubmit()); + } + public function testSetGetDynamicStepNavigationInstanceParameter() { $flow = $this->getMockedFlow(); diff --git a/Tests/IntegrationTestBundle/Controller/FormFlowController.php b/Tests/IntegrationTestBundle/Controller/FormFlowController.php index 9d3cac5d..35adc933 100644 --- a/Tests/IntegrationTestBundle/Controller/FormFlowController.php +++ b/Tests/IntegrationTestBundle/Controller/FormFlowController.php @@ -28,6 +28,18 @@ public function createTopicAction() { return $this->processFlow(new Topic(), $this->get('integrationTestBundle.form.flow.createTopic')); } + /** + * @Route("/create-topic-redirect-after-submit/", name="_FormFlow_createTopic_redirectAfterSubmit") + * @Template("IntegrationTestBundle:FormFlow:createTopic.html.twig") + */ + public function createTopicRedirectAfterSubmitAction() { + $flow = $this->get('integrationTestBundle.form.flow.createTopic'); + $flow->setAllowDynamicStepNavigation(false); + $flow->setAllowRedirectAfterSubmit(true); + + return $this->processFlow(new Topic(), $flow); + } + /** * @Route("/create-vehicle/", name="_FormFlow_createVehicle") * @Template("IntegrationTestBundle:FormFlow:createVehicle.html.twig") @@ -108,9 +120,9 @@ public function photoUploadAction() { protected function processFlow($formData, FormFlow $flow) { $flow->bind($formData); - $form = $flow->createForm(); - if ($flow->isValid($form)) { - $flow->saveCurrentStepData($form); + $form = $submittedForm = $flow->createForm(); + if ($flow->isValid($submittedForm)) { + $flow->saveCurrentStepData($submittedForm); if ($flow->nextStep()) { // create form for next step @@ -123,6 +135,14 @@ protected function processFlow($formData, FormFlow $flow) { } } + if ($flow->redirectAfterSubmit($submittedForm)) { + $request = $this->getRequest(); + $params = $this->get('craue_formflow_util')->addRouteParameters(array_merge($request->query->all(), + $request->attributes->get('_route_params')), $flow); + + return $this->redirect($this->generateUrl($request->attributes->get('_route'), $params)); + } + return array( 'form' => $form->createView(), 'flow' => $flow, diff --git a/Tests/IntegrationTestBundle/Resources/views/layout_flow.html.twig b/Tests/IntegrationTestBundle/Resources/views/layout_flow.html.twig index a91b4018..5dc1a15c 100644 --- a/Tests/IntegrationTestBundle/Resources/views/layout_flow.html.twig +++ b/Tests/IntegrationTestBundle/Resources/views/layout_flow.html.twig @@ -4,8 +4,11 @@
{{ flow.getCurrentStepNumber() }}
{{ formData | json_encode }}
{% block stepList %}{% endblock %} -
+{% set routeParams = app.request.query.all() | merge(app.request.attributes.get('_route_params')) %} +{% if flow.isAllowDynamicStepNavigation() %} + {% set routeParams = routeParams | craue_removeDynamicStepNavigationParameters(flow) %} +{% endif %} + {{ form_errors(form) }} {{ form_rest(form) }} {% include 'CraueFormFlowBundle:FormFlow:buttons.html.twig' %}