Skip to content

Commit

Permalink
Merge pull request #98 from craue/revalidate-previous-steps
Browse files Browse the repository at this point in the history
add a validation error to the current form if a form of a previous step became invalid
  • Loading branch information
craue committed Dec 17, 2013
2 parents e31d92d + 955c3ca commit cf7b778
Show file tree
Hide file tree
Showing 20 changed files with 288 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

## master

- [#98]: add a validation error to the current form if a form of a previous step became invalid
- BC breaks (follow `UPGRADE-3.0.md` to upgrade):
- [#101]: support for concurrent instances of the same flow
- removed the step field template

[#98]: https://github.com/craue/CraueFormFlowBundle/issues/98
[#101]: https://github.com/craue/CraueFormFlowBundle/issues/101

## 2.1.4 (2013-12-05)
Expand Down
88 changes: 86 additions & 2 deletions Form/FormFlow.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
use Craue\FormFlowBundle\Storage\StorageInterface;
use Craue\FormFlowBundle\Util\StringUtil;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Translation\TranslatorInterface;

/**
* @author Christian Raue <christian.raue@gmail.com>
Expand Down Expand Up @@ -44,11 +46,21 @@ abstract class FormFlow implements FormFlowInterface {
*/
protected $eventDispatcher = null;

/**
* @var TranslatorInterface
*/
protected $translator;

/**
* @var string
*/
protected $transition;

/**
* @var boolean
*/
protected $revalidatePreviousSteps = true;

/**
* @var boolean
*/
Expand Down Expand Up @@ -129,6 +141,11 @@ abstract class FormFlow implements FormFlowInterface {
*/
private $currentStepNumber = null;

/**
* @var FormInterface[]
*/
private $stepForms = array();

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -176,6 +193,13 @@ public function setEventDispatcher(EventDispatcherInterface $eventDispatcher) {
$this->eventDispatcher = $eventDispatcher;
}

/**
* {@inheritDoc}
*/
public function setTranslator(TranslatorInterface $translator) {
$this->translator = $translator;
}

public function setId($id) {
$this->id = $id;
}
Expand Down Expand Up @@ -296,6 +320,17 @@ public function getCurrentStepNumber() {
return $this->currentStepNumber;
}

public function setRevalidatePreviousSteps($revalidatePreviousSteps) {
$this->revalidatePreviousSteps = (boolean) $revalidatePreviousSteps;
}

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

public function setAllowDynamicStepNavigation($allowDynamicStepNavigation) {
$this->allowDynamicStepNavigation = (boolean) $allowDynamicStepNavigation;
}
Expand Down Expand Up @@ -597,12 +632,23 @@ public function invalidateStepData($fromStepNumber) {
protected function applyDataFromSavedSteps() {
$stepData = $this->retrieveStepData();

$this->stepForms = array();

$options = array();
if (!$this->revalidatePreviousSteps) {
$options['validation_groups'] = array(); // disable validation
}

foreach ($this->getSteps() as $step) {
$stepNumber = $step->getNumber();

if (array_key_exists($stepNumber, $stepData)) {
$stepForm = $this->createFormForStep($stepNumber);
$stepForm->bind($stepData[$stepNumber]);
$stepForm = $this->createFormForStep($stepNumber, $options);
$stepForm->bind($stepData[$stepNumber]); // the form is validated here

if ($this->revalidatePreviousSteps) {
$this->stepForms[$stepNumber] = $stepForm;
}

if ($this->hasListeners(FormFlowEvents::POST_BIND_SAVED_DATA)) {
$event = new PostBindSavedDataEvent($this, $this->formData, $stepNumber);
Expand Down Expand Up @@ -722,6 +768,27 @@ public function isValid(FormInterface $form) {
$this->eventDispatcher->dispatch(FormFlowEvents::POST_BIND_REQUEST, $event);
}

if ($this->revalidatePreviousSteps) {
// check if forms of previous steps are still valid
foreach ($this->stepForms as $stepNumber => $stepForm) {
// ignore form of the current step
if ($this->currentStepNumber === $stepNumber) {
break;
}

// ignore forms of skipped steps
if ($this->isStepSkipped($stepNumber)) {
break;
}

if (!$stepForm->isValid()) {
$form->addError($this->getPreviousStepInvalidFormError($stepNumber));

return false;
}
}
}

if ($form->isValid()) {
if ($this->hasListeners(FormFlowEvents::POST_VALIDATE)) {
$event = new PostValidateEvent($this, $form->getData());
Expand Down Expand Up @@ -790,6 +857,23 @@ protected function hasListeners($eventName) {
return $this->eventDispatcher !== null && $this->eventDispatcher->hasListeners($eventName);
}

/**
* @param integer $stepNumber
* @return FormError
*/
protected function getPreviousStepInvalidFormError($stepNumber) {
$messageId = 'craueFormFlow.previousStepInvalid';
$messageParameters = array('%stepNumber%' => $stepNumber);

if (version_compare(Kernel::VERSION, '2.2', '>=')) {
$message = $this->translator->trans($messageId, $messageParameters, 'validators');
return new FormError($message, $messageId, $messageParameters);
}

// TODO remove as soon as Symfony >= 2.2 is required
return new FormError($messageId, $messageParameters);
}

// methods for BC with third-party templates (e.g. MopaBootstrapBundle)

public function getCurrentStep() {
Expand Down
11 changes: 11 additions & 0 deletions Form/FormFlowInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Translation\TranslatorInterface;

/**
* @author Christian Raue <christian.raue@gmail.com>
Expand Down Expand Up @@ -46,6 +47,16 @@ function getStorage();
*/
function setEventDispatcher(EventDispatcherInterface $eventDispatcher);

/**
* @param TranslatorInterface $translator
*/
function setTranslator(TranslatorInterface $translator);

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

/**
* @return boolean
*/
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,23 @@ To validate the form data class a step-based validation group is passed to the f
By default, if `getName()` of the flow returns `createVehicle`, such a group is named `flow_createVehicle_step1`
for the first step.

## Disabling revalidation of previous steps

Take a look at [#98](https://github.com/craue/CraueFormFlowBundle/issues/98) for an example on why it's useful to
revalidate previous steps by default. But if you want (or need) to avoid revalidating previous steps, 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 $revalidatePreviousSteps = false;

// ...

}
```

## Passing step-based options to the form type

If your form type needs options to build the form (e.g. conditional fields) you can override method `getFormOptions`
Expand Down
3 changes: 3 additions & 0 deletions Resources/config/form_flow.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
<call method="setEventDispatcher">
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
</call>
<call method="setTranslator">
<argument type="service" id="translator" />
</call>
</service>

<service id="craue.form.flow.form_extension" class="Craue\FormFlowBundle\Form\Extension\FormFlowFormExtension">
Expand Down
1 change: 1 addition & 0 deletions Resources/translations/validators.de.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
craueFormFlow.previousStepInvalid: Das Formular für Schritt %stepNumber% ist ungültig. Gehen Sie bitte zurück und versuchen das Formular erneut zu senden.
1 change: 1 addition & 0 deletions Resources/translations/validators.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
craueFormFlow.previousStepInvalid: The form for step %stepNumber% is invalid. Please go back and try to submit it again.
1 change: 1 addition & 0 deletions Resources/translations/validators.es.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
craueFormFlow.previousStepInvalid: El formulario del paso %stepNumber% está inválido. Por favor, utilice el botón de atrás y pruebe de enviar el formulario de nuevo.
1 change: 1 addition & 0 deletions Resources/translations/validators.fr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
craueFormFlow.previousStepInvalid: Le formulaire pour l'étape %stepNumber% est invalide. Veuillez retourner et essayez de le renvoyer.
1 change: 1 addition & 0 deletions Resources/translations/validators.nl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
craueFormFlow.previousStepInvalid: Het formulier voor stap %stepNumber% is ongeldig. Ga terug en probeer het opnieuw te versturen.
1 change: 1 addition & 0 deletions Resources/translations/validators.pl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
craueFormFlow.previousStepInvalid: Formularz dla kroku %stepNumber% został wypełniony nieprawidłowo. Proszę cofnąć się i wypełnić go ponownie.
1 change: 1 addition & 0 deletions Resources/translations/validators.ru.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
craueFormFlow.previousStepInvalid: Форма для шага %stepNumber% имеет недопустимое значение. Пожалуйста, вернитесь назад и попробуйте повторить отправку.
1 change: 1 addition & 0 deletions Resources/translations/validators.uk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
craueFormFlow.previousStepInvalid: Форма для кроку %stepNumber% є недопустимою. Будь ласка, поверніться та спробуйте повторно відправити форму.
1 change: 1 addition & 0 deletions Resources/translations/validators.zh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
craueFormFlow.previousStepInvalid: 步驟%stepNumber%的表格無效,請回到上一個步驟並重新提交。
13 changes: 13 additions & 0 deletions Tests/IntegrationTestBundle/Controller/FormFlowController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Craue\FormFlowBundle\Form\FormFlow;
use Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity\Issue64Data;
use Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity\RevalidatePreviousStepsData;
use Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity\Topic;
use Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity\Vehicle;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
Expand Down Expand Up @@ -59,6 +60,18 @@ public function issue87Action() {
return $this->processFlow((object) array(), $this->get('integrationTestBundle.form.flow.issue87'));
}

/**
* @Route("/revalidatePreviousSteps/enabled/", defaults={"enabled"=true}, name="_FormFlow_revalidatePreviousSteps_enabled")
* @Route("/revalidatePreviousSteps/disabled/", defaults={"enabled"=false}, name="_FormFlow_revalidatePreviousSteps_disabled")
* @Template("IntegrationTestBundle:FormFlow:revalidatePreviousSteps.html.twig")
*/
public function revalidatePreviousStepsAction($enabled) {
$flow = $this->get('integrationTestBundle.form.flow.revalidatePreviousSteps');
$flow->setRevalidatePreviousSteps($enabled);

return $this->processFlow(new RevalidatePreviousStepsData(), $flow);
}

/**
* @Route("/skipFirstStepUsingClosure/", name="_FormFlow_skipFirstStepUsingClosure")
* @Template("IntegrationTestBundle:FormFlow:skipFirstStepUsingClosure.html.twig")
Expand Down
31 changes: 31 additions & 0 deletions Tests/IntegrationTestBundle/Entity/RevalidatePreviousStepsData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Craue\FormFlowBundle\Tests\IntegrationTestBundle\Entity;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\ExecutionContext;

/**
* @author Christian Raue <christian.raue@gmail.com>
* @copyright 2011-2013 Christian Raue
* @license http://opensource.org/licenses/mit-license.php MIT License
*
* @Assert\Callback(methods={"isDataValid"}, groups={"flow_revalidatePreviousSteps_step1"})
*/
class RevalidatePreviousStepsData {

private static $validationCalls;

public static function resetValidationCalls() {
self::$validationCalls = 0;
}

// TODO replace with ExecutionContextInterface as soon as Symfony >= 2.2 is required
public function isDataValid(ExecutionContext $context) {
// valid only on first call
if (++self::$validationCalls > 1) {
$context->addViolation('Take this!');
}
}

}
38 changes: 38 additions & 0 deletions Tests/IntegrationTestBundle/Form/RevalidatePreviousStepsFlow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Craue\FormFlowBundle\Tests\IntegrationTestBundle\Form;

use Craue\FormFlowBundle\Form\FormFlow;

/**
* @author Christian Raue <christian.raue@gmail.com>
* @copyright 2011-2013 Christian Raue
* @license http://opensource.org/licenses/mit-license.php MIT License
*/
class RevalidatePreviousStepsFlow extends FormFlow {

/**
* {@inheritDoc}
*/
public function getName() {
return 'revalidatePreviousSteps';
}

/**
* {@inheritDoc}
*/
protected function loadStepsConfig() {
return array(
array(
'label' => 'step1',
),
array(
'label' => 'step2',
),
array(
'label' => 'step3',
),
);
}

}
6 changes: 6 additions & 0 deletions Tests/IntegrationTestBundle/Resources/config/form_flow.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@
scope="request">
</service>

<service id="integrationTestBundle.form.flow.revalidatePreviousSteps"
class="Craue\FormFlowBundle\Tests\IntegrationTestBundle\Form\RevalidatePreviousStepsFlow"
parent="craue.form.flow"
scope="request">
</service>

<service id="integrationTestBundle.form.flow.skipFirstStepUsingClosure"
class="Craue\FormFlowBundle\Tests\IntegrationTestBundle\Form\SkipFirstStepUsingClosureFlow"
parent="craue.form.flow"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends 'IntegrationTestBundle::layout_flow.html.twig' %}
Loading

0 comments on commit cf7b778

Please sign in to comment.