Skip to content

Commit

Permalink
added support for the "redirect after submit" pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
craue committed Aug 11, 2014
1 parent 29bf492 commit 0bf91b7
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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

[#98]: https://github.com/craue/CraueFormFlowBundle/issues/98
[#101]: https://github.com/craue/CraueFormFlowBundle/issues/101
Expand All @@ -27,6 +28,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

## 2.1.5 (2014-06-13)
Expand Down
41 changes: 38 additions & 3 deletions Form/FormFlow.php
Expand Up @@ -60,6 +60,11 @@ abstract class FormFlow implements FormFlowInterface {
*/
protected $allowDynamicStepNavigation = false;

/**
* @var boolean
*/
protected $allowRedirectAfterSubmit = false;

/**
* @var string
*/
Expand Down Expand Up @@ -335,6 +340,17 @@ public function isAllowDynamicStepNavigation() {
return $this->allowDynamicStepNavigation;
}

public function setAllowRedirectAfterSubmit($allowRedirectAfterSubmit) {
$this->allowRedirectAfterSubmit = (boolean) $allowRedirectAfterSubmit;
}

/**
* {@inheritDoc}
*/
public function isAllowRedirectAfterSubmit() {
return $this->allowRedirectAfterSubmit;
}

public function setDynamicStepNavigationInstanceParameter($dynamicStepNavigationInstanceParameter) {
$this->dynamicStepNavigationInstanceParameter = $dynamicStepNavigationInstanceParameter;
}
Expand Down Expand Up @@ -467,7 +483,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;
}
Expand Down Expand Up @@ -548,7 +564,7 @@ protected function determineInstanceId() {

$instanceId = null;

if ($this->allowDynamicStepNavigation) {
if ($this->allowDynamicStepNavigation || $this->allowRedirectAfterSubmit) {
$instanceId = $request->get($this->getDynamicStepNavigationInstanceParameter());
}

Expand All @@ -562,7 +578,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;
}

Expand Down Expand Up @@ -811,6 +827,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 && $this->getRequest()->isMethod('POST')) {
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
Expand Down
5 changes: 5 additions & 0 deletions Form/FormFlowInterface.php
Expand Up @@ -56,6 +56,11 @@ function isRevalidatePreviousSteps();
*/
function isAllowDynamicStepNavigation();

/**
* @return boolean
*/
function isAllowRedirectAfterSubmit();

/**
* @return string
*/
Expand Down
45 changes: 44 additions & 1 deletion README.md
Expand Up @@ -10,7 +10,8 @@ Features:
- step labels
- skipping of steps
- different validation group for each step
- 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/.

Expand Down Expand Up @@ -540,6 +541,48 @@ you should modify the opening form tag in the form template like this:
app.request.query.all | craue_removeDynamicStepNavigationParameters(flow)) }}" {{ form_enctype(form) }}>
```

## 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:
Expand Down
97 changes: 97 additions & 0 deletions Tests/CreateTopicFlowTest.php
Expand Up @@ -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"}');
}

}
22 changes: 22 additions & 0 deletions Tests/Form/FormFlowTest.php
Expand Up @@ -328,6 +328,28 @@ public function dataSetIsAllowDynamicStepNavigation() {
);
}

/**
* @dataProvider dataSetIsAllowRedirectAfterSubmit
*/
public function testSetIsAllowRedirectAfterSubmit($expectedValue, $allowRedirectAfterSubmit) {
/* @var $flow \PHPUnit_Framework_MockObject_MockObject|\Craue\FormFlowBundle\Form\FormFlow */
$flow = $this->getMockForAbstractClass('\Craue\FormFlowBundle\Form\FormFlow');

$flow->setAllowRedirectAfterSubmit($allowRedirectAfterSubmit);

$this->assertEquals($expectedValue, $flow->isAllowRedirectAfterSubmit());
}

public function dataSetIsAllowRedirectAfterSubmit() {
return array(
array(true, true),
array(false, false),
array(true, 1),
array(false, 0),
array(false, null),
);
}

public function testSetGetDynamicStepNavigationInstanceParameter() {
/* @var $flow \PHPUnit_Framework_MockObject_MockObject|\Craue\FormFlowBundle\Form\FormFlow */
$flow = $this->getMockForAbstractClass('\Craue\FormFlowBundle\Form\FormFlow');
Expand Down
26 changes: 23 additions & 3 deletions Tests/IntegrationTestBundle/Controller/FormFlowController.php
Expand Up @@ -27,6 +27,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")
Expand Down Expand Up @@ -99,9 +111,9 @@ public function onlyOneStepAction() {
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
Expand All @@ -114,6 +126,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,
Expand Down
Expand Up @@ -4,8 +4,11 @@
<div id="step-number">{{ flow.getCurrentStepNumber() }}</div>
<div id="form-data">{{ formData | json_encode }}</div>
{% block stepList %}{% endblock %}
<form method="post" action="{{ path(app.request.attributes.get('_route'),
app.request.attributes.get('_route_params') | craue_removeDynamicStepNavigationParameters(flow)) }}" {{ form_enctype(form) }}>
{% set routeParams = app.request.query.all() | merge(app.request.attributes.get('_route_params')) %}
{% if flow.isAllowDynamicStepNavigation() %}
{% set routeParams = routeParams | craue_removeDynamicStepNavigationParameters(flow) %}
{% endif %}
<form method="post" action="{{ path(app.request.attributes.get('_route'), routeParams) }}" {{ form_enctype(form) }}>
{{ form_errors(form) }}
{{ form_rest(form) }}
{% include 'CraueFormFlowBundle:FormFlow:buttons.html.twig' %}
Expand Down

0 comments on commit 0bf91b7

Please sign in to comment.