Giter Site home page Giter Site logo

craueformflowbundle's Introduction

Information

Tests Coverage Status

CraueFormFlowBundle provides a facility for building and handling multi-step forms in your Symfony project.

Features:

  • navigation (next, back, start over)
  • step labels
  • skipping of steps
  • different validation group for each step
  • handling of file uploads
  • 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/symfony-playground/en/CraueFormFlow/.

Installation

Get the bundle

Let Composer download and install the bundle by running

composer require craue/formflow-bundle

in a shell.

Enable the bundle

If you don't use Symfony Flex, register the bundle manually:

// in config/bundles.php
return [
	// ...
	Craue\FormFlowBundle\CraueFormFlowBundle::class => ['all' => true],
];

Or, for Symfony 3.4:

// in app/AppKernel.php
public function registerBundles() {
	$bundles = [
		// ...
		new Craue\FormFlowBundle\CraueFormFlowBundle(),
	];
	// ...
}

Usage

This section shows how to create a 3-step form flow for creating a vehicle. You have to choose between two approaches on how to set up your flow.

Approach A: One form type for the entire flow

This approach makes it easy to turn an existing (common) form into a form flow.

Create a flow class

// src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
use Craue\FormFlowBundle\Form\FormFlow;
use Craue\FormFlowBundle\Form\FormFlowInterface;
use MyCompany\MyBundle\Form\CreateVehicleForm;

class CreateVehicleFlow extends FormFlow {

	protected function loadStepsConfig() {
		return [
			[
				'label' => 'wheels',
				'form_type' => CreateVehicleForm::class,
			],
			[
				'label' => 'engine',
				'form_type' => CreateVehicleForm::class,
				'skip' => function($estimatedCurrentStepNumber, FormFlowInterface $flow) {
					return $estimatedCurrentStepNumber > 1 && !$flow->getFormData()->canHaveEngine();
				},
			],
			[
				'label' => 'confirmation',
			],
		];
	}

}

Create a form type class

You only have to create one form type class for a flow. There is an option called flow_step you can use to decide which fields will be added to the form according to the step to render.

// src/MyCompany/MyBundle/Form/CreateVehicleForm.php
use MyCompany\MyBundle\Form\Type\VehicleEngineType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;

class CreateVehicleForm extends AbstractType {

	public function buildForm(FormBuilderInterface $builder, array $options) {
		switch ($options['flow_step']) {
			case 1:
				$validValues = [2, 4];
				$builder->add('numberOfWheels', ChoiceType::class, [
					'choices' => array_combine($validValues, $validValues),
					'placeholder' => '',
				]);
				break;
			case 2:
				// This form type is not defined in the example.
				$builder->add('engine', VehicleEngineType::class, [
					'placeholder' => '',
				]);
				break;
		}
	}

	public function getBlockPrefix() {
		return 'createVehicle';
	}

}

Approach B: One form type per step

This approach makes it easy to reuse the form types to compose other forms.

Create a flow class

// src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
use Craue\FormFlowBundle\Form\FormFlow;
use Craue\FormFlowBundle\Form\FormFlowInterface;
use MyCompany\MyBundle\Form\CreateVehicleStep1Form;
use MyCompany\MyBundle\Form\CreateVehicleStep2Form;

class CreateVehicleFlow extends FormFlow {

	protected function loadStepsConfig() {
		return [
			[
				'label' => 'wheels',
				'form_type' => CreateVehicleStep1Form::class,
			],
			[
				'label' => 'engine',
				'form_type' => CreateVehicleStep2Form::class,
				'skip' => function($estimatedCurrentStepNumber, FormFlowInterface $flow) {
					return $estimatedCurrentStepNumber > 1 && !$flow->getFormData()->canHaveEngine();
				},
			],
			[
				'label' => 'confirmation',
			],
		];
	}

}

Create form type classes

// src/MyCompany/MyBundle/Form/CreateVehicleStep1Form.php
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;

class CreateVehicleStep1Form extends AbstractType {

	public function buildForm(FormBuilderInterface $builder, array $options) {
		$validValues = [2, 4];
		$builder->add('numberOfWheels', ChoiceType::class, [
			'choices' => array_combine($validValues, $validValues),
			'placeholder' => '',
		]);
	}

	public function getBlockPrefix() {
		return 'createVehicleStep1';
	}

}
// src/MyCompany/MyBundle/Form/CreateVehicleStep2Form.php
use MyCompany\MyBundle\Form\Type\VehicleEngineType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class CreateVehicleStep2Form extends AbstractType {

	public function buildForm(FormBuilderInterface $builder, array $options) {
		$builder->add('engine', VehicleEngineType::class, [
			'placeholder' => '',
		]);
	}

	public function getBlockPrefix() {
		return 'createVehicleStep2';
	}

}

Register your flow as a service

XML

<services>
	<service id="myCompany.form.flow.createVehicle"
			class="MyCompany\MyBundle\Form\CreateVehicleFlow"
			autoconfigure="true">
	</service>
</services>

YAML

services:
    myCompany.form.flow.createVehicle:
        class: MyCompany\MyBundle\Form\CreateVehicleFlow
        autoconfigure: true

When not using autoconfiguration, you may let your flow inherit the required dependencies from a parent service.

XML

<services>
	<service id="myCompany.form.flow.createVehicle"
			class="MyCompany\MyBundle\Form\CreateVehicleFlow"
			parent="craue.form.flow">
	</service>
</services>

YAML

services:
    myCompany.form.flow.createVehicle:
        class: MyCompany\MyBundle\Form\CreateVehicleFlow
        parent: craue.form.flow

Create a form template

You only need one template for a flow. The instance of your flow class is passed to the template in a variable called flow so you can use it to render the form according to the current step.

{# in src/MyCompany/MyBundle/Resources/views/Vehicle/createVehicle.html.twig #}
<div>
	Steps:
	{% include '@CraueFormFlow/FormFlow/stepList.html.twig' %}
</div>
{{ form_start(form) }}
	{{ form_errors(form) }}

	{% if flow.getCurrentStepNumber() == 1 %}
		<div>
			When selecting four wheels you have to choose the engine in the next step.<br />
			{{ form_row(form.numberOfWheels) }}
		</div>
	{% endif %}

	{{ form_rest(form) }}

	{% include '@CraueFormFlow/FormFlow/buttons.html.twig' %}
{{ form_end(form) }}

CSS

Some CSS is needed to render the buttons correctly. Load the provided file in your base template:

<link type="text/css" rel="stylesheet" href="{{ asset('bundles/craueformflow/css/buttons.css') }}" />

...and install the assets in your project:

# in a shell
php bin/console assets:install --symlink web

Buttons

You can customize the default button look by using these variables to add one or more CSS classes to them:

  • craue_formflow_button_class_last will apply either to the next or finish button
  • craue_formflow_button_class_finish will specifically apply to the finish button
  • craue_formflow_button_class_next will specifically apply to the next button
  • craue_formflow_button_class_back will apply to the back button
  • craue_formflow_button_class_reset will apply to the reset button

Example with Bootstrap button classes:

{% include '@CraueFormFlow/FormFlow/buttons.html.twig' with {
		craue_formflow_button_class_last: 'btn btn-primary',
		craue_formflow_button_class_back: 'btn',
		craue_formflow_button_class_reset: 'btn btn-warning',
	} %}

In the same manner you can customize the button labels:

  • craue_formflow_button_label_last for either the next or finish button
  • craue_formflow_button_label_finish for the finish button
  • craue_formflow_button_label_next for the next button
  • craue_formflow_button_label_back for the back button
  • craue_formflow_button_label_reset for the reset button

Example:

{% include '@CraueFormFlow/FormFlow/buttons.html.twig' with {
		craue_formflow_button_label_finish: 'submit',
		craue_formflow_button_label_reset: 'reset the flow',
	} %}

You can also remove the reset button by setting craue_formflow_button_render_reset to false.

Create an action

// in src/MyCompany/MyBundle/Controller/VehicleController.php
public function createVehicleAction() {
	$formData = new Vehicle(); // Your form data class. Has to be an object, won't work properly with an array.

	$flow = $this->get('myCompany.form.flow.createVehicle'); // must match the flow's service id
	$flow->bind($formData);

	// form of the current step
	$form = $flow->createForm();
	if ($flow->isValid($form)) {
		$flow->saveCurrentStepData($form);

		if ($flow->nextStep()) {
			// form for the next step
			$form = $flow->createForm();
		} else {
			// flow finished
			$em = $this->getDoctrine()->getManager();
			$em->persist($formData);
			$em->flush();

			$flow->reset(); // remove step data from the session

			return $this->redirectToRoute('home'); // redirect when done
		}
	}

	return $this->render('@MyCompanyMy/Vehicle/createVehicle.html.twig', [
		'form' => $form->createView(),
		'flow' => $flow,
	]);
}

Explanations

How the flow works

  1. Dispatch PreBindEvent.
  2. Dispatch GetStepsEvent.
  3. Update the form data class with previously saved data of all steps. For each one, dispatch PostBindSavedDataEvent.
  4. Evaluate which steps are skipped. Determine the current step.
  5. Dispatch PostBindFlowEvent.
  6. Create the form for the current step.
  7. Bind the request to that form.
  8. Dispatch PostBindRequestEvent.
  9. Validate the form data.
  10. Dispatch PostValidateEvent.
  11. Save the form data.
  12. Proceed to the next step.

Method loadStepsConfig

The array returned by that method is used to create all steps of the flow. The first item will be the first step. You can, however, explicitly index the array for easier readability.

Valid options per step are:

  • label (string|StepLabel|null)
    • If you'd like to render an overview of all steps you have to set the label option for each step.
    • If using a callable on a StepLabel instance, it has to return a string value or null.
    • By default, the labels will be translated using the messages domain when rendered in Twig.
  • form_type (FormTypeInterface|string|null)
    • The form type used to build the form for that step.
    • This value is passed to Symfony's form factory, thus the same rules apply as for creating common forms. If using a string, it has to be the FQCN of the form type.
  • form_options (array)
    • Options passed to the form type of that step.
  • skip (callable|bool)
    • Decides whether the step will be skipped.
    • If using a callable...
      • it will receive the estimated current step number and the flow as arguments;
      • it has to return a boolean value;
      • it might be called more than once until the actual current step number has been determined.

Examples

protected function loadStepsConfig() {
	return [
		[
			'form_type' => CreateVehicleStep1Form::class,
		],
		[
			'form_type' => CreateVehicleStep2Form::class,
			'skip' => true,
		],
		[
		],
	];
}
protected function loadStepsConfig() {
	return [
		1 =>[
			'label' => 'wheels',
			'form_type' => CreateVehicleStep1Form::class,
		],
		2 => [
			'label' => StepLabel::createCallableLabel(function() { return 'engine'; })
			'form_type' => CreateVehicleStep2Form::class,
			'form_options' => [
				'validation_groups' => ['Default'],
			],
			'skip' => function($estimatedCurrentStepNumber, FormFlowInterface $flow) {
				return $estimatedCurrentStepNumber > 1 && !$flow->getFormData()->canHaveEngine();
			},
		],
		3 => [
			'label' => 'confirmation',
		],
	];
}

Advanced stuff

Validation groups

To validate the form data class bound to the flow, a step-based validation group is passed to the form type. By default, if the flow's getName method returns createVehicle, such a group is named flow_createVehicle_step1 for the first step. You can customize this name by setting the flow's property validationGroupPrefix explicitly. The step number (1, 2, 3, etc.) will be appended by the flow.

Compared to standalone forms, setting the validation_groups option in your form type's configureOptions method won't have any effect in the context of a flow. The value is just ignored, i.e. will be overwritten by the flow. But there are other ways of defining custom validation groups:

  • override the flow's getFormOptions method,
  • use the form_options step option, or
  • use the flow's setGenericFormOptions method.

The generated step-based validation group will be added by the flow, unless the validation_groups option is set to false, a closure, or a GroupSequence. In this case, it will not be added by the flow, so ensure the step forms are validated as expected.

Disabling revalidation of previous steps

Take a look at #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, add this to your flow class:

// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
class CreateVehicleFlow extends FormFlow {

	protected $revalidatePreviousSteps = false;

	// ...

}

Passing generic options to the form type

To set options common for the form type(s) of all steps you can use method setGenericFormOptions:

// in src/MyCompany/MyBundle/Controller/VehicleController.php
public function createVehicleAction() {
	// ...
	$flow->setGenericFormOptions(['action' => 'targetUrl']);
	$flow->bind($formData);
	$form = $flow->createForm();
	// ...
}

Passing step-based options to the form type

To pass individual options to each step's form type you can use the step config option form_options:

// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
protected function loadStepsConfig() {
	return [
		[
			'label' => 'wheels',
			'form_type' => CreateVehicleStep1Form:class,
			'form_options' => [
				'validation_groups' => ['Default'],
			],
		],
	];
}

Alternatively, to set options based on previous steps (e.g. to render fields depending on submitted data) you can override method getFormOptions of your flow class:

// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
public function getFormOptions($step, array $options = []) {
	$options = parent::getFormOptions($step, $options);

	$formData = $this->getFormData();

	if ($step === 2) {
		$options['numberOfWheels'] = $formData->getNumberOfWheels();
	}

	return $options;
}

Enabling dynamic step navigation

Dynamic step navigation means that the step list rendered will contain links to go back/forth to a specific step (which has been done already) directly. To enable it, add this to your flow class:

// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
class CreateVehicleFlow extends FormFlow {

	protected $allowDynamicStepNavigation = true;

	// ...

}

If you'd like to remove the parameters (added by using such a direct link) when submitting the form you should modify the action for the opening form tag in the template like this:

{{ form_start(form, {'action': path(app.request.attributes.get('_route'),
		app.request.query.all | craue_removeDynamicStepNavigationParameters(flow))}) }}

Handling of file uploads

File uploads are transparently handled by Base64-encoding the content and storing it in the session, so it may affect performance. This feature is enabled by default for convenience, but can be disabled in the flow class as follows:

// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
class CreateVehicleFlow extends FormFlow {

	protected $handleFileUploads = false;

	// ...

}

By default, the system's directory for temporary files will be used for files restored from the session while loading step data. You can set a custom one:

// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
class CreateVehicleFlow extends FormFlow {

	protected $handleFileUploadsTempDir = '/path/for/flow/uploads';

	// ...

}

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, add this to your flow class:

// 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:

// 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->redirectToRoute($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:

// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
use Craue\FormFlowBundle\Event\GetStepsEvent;
use Craue\FormFlowBundle\Event\PostBindFlowEvent;
use Craue\FormFlowBundle\Event\PostBindRequestEvent;
use Craue\FormFlowBundle\Event\PostBindSavedDataEvent;
use Craue\FormFlowBundle\Event\PostValidateEvent;
use Craue\FormFlowBundle\Event\PreBindEvent;
use Craue\FormFlowBundle\Form\FormFlowEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CreateVehicleFlow extends FormFlow implements EventSubscriberInterface {

	/**
	 * This method is only needed when _not_ using autoconfiguration. If it's there even with autoconfiguration enabled,
	 * the `removeSubscriber` call ensures that subscribed events won't occur twice.
	 * (You can remove the `removeSubscriber` call if you'll definitely never use autoconfiguration for that flow.)
	 */
	public function setEventDispatcher(EventDispatcherInterface $dispatcher) {
		parent::setEventDispatcher($dispatcher);
		$dispatcher->removeSubscriber($this);
		$dispatcher->addSubscriber($this);
	}

	public static function getSubscribedEvents() {
		return [
			FormFlowEvents::PRE_BIND => 'onPreBind',
			FormFlowEvents::GET_STEPS => 'onGetSteps',
			FormFlowEvents::POST_BIND_SAVED_DATA => 'onPostBindSavedData',
			FormFlowEvents::POST_BIND_FLOW => 'onPostBindFlow',
			FormFlowEvents::POST_BIND_REQUEST => 'onPostBindRequest',
			FormFlowEvents::POST_VALIDATE => 'onPostValidate',
		];
	}

	public function onPreBind(PreBindEvent $event) {
		// ...
	}

	public function onGetSteps(GetStepsEvent $event) {
		// ...
	}

	public function onPostBindSavedData(PostBindSavedDataEvent $event) {
		// ...
	}

	public function onPostBindFlow(PostBindFlowEvent $event) {
		// ...
	}

	public function onPostBindRequest(PostBindRequestEvent $event) {
		// ...
	}

	public function onPostValidate(PostValidateEvent $event) {
		// ...
	}

	// ...

}

craueformflowbundle's People

Contributors

birko avatar craue avatar dafish avatar doncallisto avatar franmomu avatar fullpipe avatar haase-fabian avatar hason avatar havvg avatar jayesbe avatar jderusse avatar kaiwa avatar kklecho avatar koc avatar konrados avatar mohebifar avatar nyholm avatar oskarstark avatar paddya avatar piotrantosik avatar pulzarraider avatar rvanlaak avatar siliconengine avatar sprankhub avatar sveriger avatar thomaslandauer avatar tristanbes avatar ubermuda avatar warxcell avatar xthiago avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

craueformflowbundle's Issues

Catchable Fatal Error: Argument 1 passed to Craue\FormFlowBundle\Form\FormFlow::setStorage()

With the current master of symfony 2.1 and CraueFormFlowBundle i get this error:

Catchable Fatal Error: Argument 1 passed to Craue\FormFlowBundle\Form\FormFlow::setStorage()
must implement interface Craue\FormFlowBundle\Storage\StorageInterface,
instance of Symfony\Component\HttpFoundation\Session\Session given,
called in /.../app/cache/dev/appDevDebugProjectContainer.php on line 612 and 
defined in /.../vendor/craue/formflow-bundle/Craue/FormFlowBundle/Form/FormFlow.php line 130

Problem uploading file

I have the following Entity (not Doctrine Entity, just a plain class):

class RegistrationEntity {
    private $photo;
    ...

    public function setPhoto($photo) {
        if($photo instanceof UploadedFile) {
            $this->photo = $photo->move('/tmp', 'foo.jpg')->getPathname();
        }
        else {
            $this->photo = $photo;
        }
    }
 }

With my action I'm trying to upload the photo on step 2 (of 3):

 public function accountantAction() {
    $values = new RegistrationEntity(); 

    //Manage multi-step form:
    $flow = $this->get('longtale.form.flow.registrationFlow'); // must match the flow's service id
    $flow->bind($values);

    $form = $flow->createForm($values);

    if ($flow->isValid($form)) {
              $flow->saveCurrentStepData();

              if ($flow->nextStep()) {

              // render form for next step
              return array(
                  'form' => $flow->createForm($values)->createView(),
                  'flow' => $flow,
             );
         }

         //All steps completed: 
         die(var_dump($values));

         $flow->reset();
         return $this->redirect($this->generateUrl('home')); // redirect when done
      }

      return array('form' => $form->createView(), 'flow' => $flow);
}

However in the next step the $photo property with the temp filename of the uploaded file get lost, I dump the entity and the value $photo is null (while the others are ok).

in postValidate e preBind events of my Flow class the value is present when I submit the step2 form, but it disappear in the next step.

Any clue?

Thanks!

How to get an field not mapped?

Hi,

In my form I have an choice field not mapped to entity ('mapped' => false). The method FormFlow::getFormOptions() receives $formData, but that only contains data of entity. How to get value of these field not mapped?

Using FormFlow with one (login/register) step with FOSUserBundle

I am actually developing and e-commerce site where the validation order has 4 steps

  • OrderValidation
  • Adress to deliver
  • register or login (optional step if logged)
  • validation

I am wonderring if it is possible to make the login/register work in the bundle and go to the 4th steps.

Any idea?

Thx folks

Improve step number

An exception doesn't make any sense to me here:

throw new \RuntimeException('The current step has not been determined yet and thus cannot be accessed.');

public function getCurrentStepNumber() {
        if ($this->currentStepNumber === null) {
            throw new \RuntimeException('The current step has not been determined yet and thus cannot be accessed.');
        }

        return $this->currentStepNumber;
    }

Why throw an exception if a special sequence of events hasn't happened yet? Couldn't you just determineCurrentStepNumber() if currentStepNumber isn't already set?

Would this have a negative impact that I'm overlooking?

FormFlow::isStepDone error

If you create a flow with dynamic step navigation, FormFlow::isStepDone returns an error when the flow is created for the very first time.
The following message is returned:
"Warning: array_key_exists() expects parameter 2 to be array, null given in /vendor/bundles/Craue/FormFlowBundle/Form/FormFlow.php line 157"

I think you should test that variable $sessionData is not null and then check if there is any data in session.

public function isStepDone($step) {
    $sessionData = $this->session->get($this->sessionDataKey);
    return ($sessionData && array_key_exists($step, $sessionData));
}

Direct Step Links not passing GET parameters

I have a scenario where I have a URL (/albums/edit/1) and when I click on a specific step in my form flow it doesn't pass the ID ( in this case 1) into the route, and I receive an error from symfony2 saying that there is a missing parameter ID for the route.

In stepList_content.html.twig shouldn't the code below take care of this?

<a href="{{ path(app.request.attributes.get('_route'),
                            app.request.query.all | craue_addDynamicStepNavigationParameter(flow, loop.index)) }}">

replacement of setCurrentStep() in 2.x branch

I note that the method setCurrentStep() has been removed from the FormFlow in the 2.x branch - is there another available method to set the step from the outside, or is one now required to add skip closures to the steps config in order to select the correct step?

Is UniqueEntity validation supported?

I have a UniqueEntity-validation in one of my entities. However, this validation is not triggered by the flow. Anything special about it or have I overlooked something?

Entity.php:

<?php

// ... namespace, use etc.

/**
 * @UniqueEntity(fields={"name", "type"}, message="Artist already available.")
 */

// ... rest of the class

Need way to return to a step after credit card decline

I have a three-step form:

  1. Enter name and address
  2. Enter credit card details
  3. Show confirmation details and FINISH button

I have the normal NEXT/BACK buttons working fine. It also works fine with the FINISH button if the card is approved.

However, I cannot figure out how to return to Step 2 if the credit card is declined after hitting the "FINISH" button.

I want to be able to return to step 2 and display the form data, along with an error message (so the user can retry their card).

There may be an easy way to do this, but I have not been able to figure it out. Ideally, I would like a setCurrentStepNumber() method that would work something like this:

$formData = new OrderData();

$flow = $this->get('myCompany.form.flow.placeOrder');
$flow->bind($formData);

$form = $flow->createForm();
if ($flow->isValid($form)) {
   $flow->saveCurrentStepData($form);

   if ($flow->nextStep()) {
       $form = $flow->createForm();
   } else {
       $result = $this->processOrder($formData);

       if ($result == "OK") {
           $flow->reset();
           return $this->redirect($this->generateUrl('sale_completed'));
       } else {
           $flow->setCurrentStepNumber(2);   // return to step 2
           $this->get('session')->getFlashBag()->add('flash-notice', 'Card Declined');
       }
   }
}

Is this the good way to handle file upload with preview ?

Hi, I want to upload an image in the first step and then show this image in the second step. (In fact my real goal is to let the user crop and resize the image in the second step)

So I have to create a temporaly file. Remove every unused temporaly files when the image is changed or the flow is reseted.
What do you think about my code ? Do you think there is something simplier than my code ?

Maybe we can add a common code to handle upload file directly with the bundle. Plus I note that files are not stored in the session with the bind method.

Note: I use PHP 5.4

services.xml

<service id="MyProject.form.addNews"
        class="MyProject\BackBundle\Form\AddNewsFormType">
    <tag name="form.type" alias="addNews" />
</service>

<service id="MyProject.form.flow.addNews"
        class="MyProject\BackBundle\Form\AddNewsFlow"
        parent="craue.form.flow"
        scope="request">
    <call method="setFormType">
        <argument type="service" id="MyProject.form.addNews" />
    </call>
    <call method="setEntityManager">
        <argument type="service" id="doctrine.orm.entity_manager" />
    </call>
</service>

FormFlow

class AddNewsFlow extends FormFlow
{
    protected $maxSteps = 2;

    protected $allowDynamicStepNavigation = true;

    protected $em;

    protected function loadStepDescriptions()
    {
        return [
            'Step 1',
            'Step 2',
        ];
    }

    public function setEntityManager(EntityManager $em)
    {
        $this->em = $em;
    }

    public function reset()
    {
        $image = $this->storage->get($this->stepDataKey.'_image', []);
        if (!empty($image)) {
            $tempFile = $this->em->getRepository('MyProjectBackBundle:TempFile')->find($image['id']);
            if ($tempFile !== null) {
                $this->em->remove($tempFile);
                $this->em->flush();
            }
            $this->storage->remove($this->stepDataKey.'_image');
        }

        parent::reset();
    }
}

FormType

class AddNewsFormType extends AbstractType {

    public function buildForm(FormBuilderInterface $builder, array $options) {
        switch ($options['flowStep']) {
            case 1:
                // Avoid to ask the user to reload an image if the first step is display again
                $builder->add('image', 'file', ['required' => $options['image_required']]);
                break;
            case 2:
                $builder->add('X','integer', ['mapped' => false, 'attr' => array('value' => 0)]);
                $builder->add('Y','integer', ['mapped' => false, 'attr' => array('value' => 0)]);
                $builder->add('Zoom','integer', ['mapped' => false, 'attr' => array('value' => 0)]);
                break;
                break;
        }
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver) {
        $resolver->setDefaults(array(
            'flowStep' => 1,
            'data_class' => 'MyProject\CoreBundle\Entity\News', // should point to your user entity
            'image_required' => true
        ));
    }

    public function getName() {
        return 'addNews';
    }
}

Template add.html.twig

<div>
    Steps:
    {% include 'CraueFormFlowBundle:FormFlow:stepList.html.twig' %}
</div>
<form method="post" {{ form_enctype(form) }}>
    {% include 'CraueFormFlowBundle:FormFlow:stepField.html.twig' %}

    {{ form_errors(form) }}
    {{ form_rest(form) }}

    {% if image is defined and flow.getCurrentStep() <= 2 %}
        {% if flow.getCurrentStep() == 1 %}
            {# Preview of the current image when you return the the step 1 #}
            <img src="{{ image }}" width="200" />
        {% else %}
            {# show a big image to perform the crop and resize #}
            <img src="{{ image }}" width="940" />
        {% endif %}
    {% endif %}

    {% include 'CraueFormFlowBundle:FormFlow:buttons.html.twig' %}
</form>

And the controller:

public function addAction(Request $request)
{
    $news = new News();

    $flow = $this->get('MyProject.form.flow.addNews'); // must match the flow's service id
    $flow->bind($news);

    $storage = $flow->getStorage();

    $image = $storage->get($flow->getStepDataKey().'_image', ['webpath' => null]);

    // form of the current step
    $form = $flow->createForm($news, ['image_required' => !isset($image['id'])]);
    if ($flow->isValid($form)) {
        $flow->saveCurrentStepData();

        if ($flow->getCurrentStep() == 1) {
            if (null !== $file = $request->files->get('addNews')['image']) {
                if (!empty($image)) {
                    $oldTempFile = $this->getRepository('MyProjectBackBundle:TempFile')->find($image['id']));
                    if ($oldTempFile !== null) {
                        $em = $this->getDoctrine()->getEntityManager();
                        $em->remove($oldTempFile);
                        $em->flush();
                    }
                }

                $tempFile = new TempFile($file);
                $em = $this->getDoctrine()->getEntityManager();
                $em->persist($tempFile);
                $em->flush();

                $image = [ 
                    'id' => $tempFile->getId(), 
                    'path' => $tempFile->getPath(), 
                    'webpath' => $tempFile->getWebPath(),
                    'name' => $tempFile->getFilename()
                ];

                $storage->set($flow->getStepDataKey().'_image', $image);
            }
        }

        if ($flow->nextStep()) {
            // form for the next step
            $form = $flow->createForm($news, ['image_required' => !isset($image['id'])]);
        } else {
            // flow finished     
            $target = $news->getImageDir().$image['name'];            
            if (!@rename($image['path'], $target)) {
                // error
            }

            @chmod($target, 0666 & ~umask());

            $news->setImage($image['name']);
            $em = $this->getDoctrine()->getEntityManager();
            $em->persist($news);
            $em->flush();

            return $this->redirect($this->generateUrl('home')); // redirect when done
        }
    }

    return [
        'form' => $form->createView(),
        'flow' => $flow,
        'image' => $image['webpath']
    ];
}

"This form should not contain extra fields/The CSRF token is invalid. Please try to resubmit the form." Issue

Why is it that i get this issue when i jump to a phase? The following is controller code i have written to jump to a particular step.

$flow = $this->get('experiment.form.flow');
$formData = new FormData();

    //reset flow if previous was saved
    $flow->reset();

    //jump to required phase
    for($i = 1; $i < $phaseid; $i++)
    {
        if($flow->nextStep())
        {
             $form = $flow->createForm();
        }
    }

    $flow->bind($formData);

    $form = $flow->createForm();


      if ($this->getRequest()->getMethod() == 'POST') {      

          if ($flow->isValid($form)) {                                                         

                $flow->saveCurrentStepData($form);

                    if ($flow->nextStep()) {

                        // create form for next step
                        $form = $flow->createForm();
                    } else {
                    // flow finished

                    $flow->reset();

                    return $this->redirect($this->generateUrl('home'));
            }
        }
}
    $template = $flow->loadPhaseView($flow->getCurrentStepNumber());

    return $this->render(
        $template,
        array(
            'form' => $form->createView(),
            'flow' => $flow,
            'formData' => $formData,
        )
    );

    return $this->render(ExperimentBundle:Default:flow.html.twig');  

Use real events instead of methods in flow-class?

The flow class has several methods who act as events at the moment. What about moving replacing these methods with real events which can be triggered by the listener (who needs to be injected)? This would be a good move in terms of separation of concerns.

Issue with the example

I get the following error:

Neither the property "numberOfWheels" nor one of the methods "getNumberOfWheels()", "isNumberOfWheels()", "hasNumberOfWheels()", "__get()" or "__call()" exist and have public access in class "Lucky\RNDBundle\Form\CreateVehicleStep1Form"

Different bound entity based on flow step

Hi there.

I'm currently struggling to find a solution for the following: I want to change the underlying entity of a form based on a step in the flow, e.g. due to usage of class table inheritance (CTI).

Has anyone done this yet?

How to use with no entity?

I have left out the data_class line because the form I am building does not have an associated entity. How can I handle such a case with this bundle?

[Event] Add formData to PreBind

Hi,

I want to change some attributes in the data if the user is logged, so I could do that in preBind Event if I have the formData available. When the preBind event is dispatched, formData is available, so if you want I can do a PR adding formData to FormFlowEvent and refactoring the other Events.

The step "0" does not exist.

followed the instructions on the readme, but when i added a (custom) button to go back one step, I get this when I go from step 2 to step 1...

Remove Reset button & jump to step?

  1. Regarding 'Remove Reset button', Can this be done from loading in every form?

  2. Also, if i want to load the 3rd step defined in my workflow, can that be done? If so can you show me an example?

  3. I managed to override the button.next text as follows:

    {{- 'Home' | trans({}, 'CraueFormFlowBundle') -}}

But, how would i override the back button?

Error: "This form should not contain extra fields"

Hello, I'm trying to implement FormFlowBundle on my Product registration.

This form must be contain two steps.

I did all as the documentation says, but I'm got "This form should not contain extra fields" on the last step. All things are fine till I click in the finish button, then my form back to the first step with all fields in default value and with this error messages. This messagem appear one time to each step. In this case two times.

Any ideas?

Best regards,
Guilherme

Define UI for each step?

Currently i have defined my workflow and i would like to know if there is a mechanism to define the twig file for each step so that when creating the form in the controller class, it automatically includes the said twig file to master page?

Losing options when going one step back

I have a form with two steps and I need the user object to build the form in the first step.
In the controller I pass the user to the form.

$form = $flow->createForm($entity, array('user' => $user));

I set the option in the form type

public function getDefaultOptions(array $options) {
    $options = parent::getDefaultOptions($options);
        
    $options['flowStep'] = 1;
    $options['data_class'] = 'MyBundle\Entity\MyEntity';
    $options['user'] = null;
        
    return $options;
}

and I retrieve it in the buildForm function

public function buildForm(FormBuilder $builder, array $options) {
    $user = $options['user'];
        
    switch ($options['flowStep']) {
    ...

It works at the beginning, but if in the second step I click on the back button, I get an error because the user is not in options anymore.

How can I do that?

Thank you.

[event] adding form elements dynamically

Here's my scenario I am struggling with:

  1. form 1: ask for url
  2. fetch some information from the given url
  3. add an element based on the fetched information dynamically to the form using an EventSubscriber -> POST_SET_DATA
  4. form 2: display the form including the added form elements (radio button group)
  5. save everything in the database

Problem: after submitting form 2 I am getting "this form should not contain extra fields".

Should this scenario work with the Flow Bundle?

rework skipping of steps

Currently, it's quite cumbersome to implement a step which can be skipped depending on input from previous steps. As for now, I'm just thinking about how to improve this functionality and would like to see ideas from other developers as well. ;)

This are the current flow class and form class of the live demo:

<?php

namespace Craue\Symfony2PlaygroundBundle\Form;

use Craue\FormFlowBundle\Event\PostValidateEvent;
use Craue\FormFlowBundle\Event\PreBindEvent;
use Craue\FormFlowBundle\Form\FormFlow;
use Craue\FormFlowBundle\Form\FormFlowEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CreateTopicFlow extends FormFlow implements EventSubscriberInterface {

    protected $maxSteps = 4;

    /**
     * {@inheritDoc}
     */
    public function setEventDispatcher(EventDispatcherInterface $dispatcher) {
        parent::setEventDispatcher($dispatcher);
        $dispatcher->addSubscriber($this);
    }

    /**
     * {@inheritDoc}
     */
    public static function getSubscribedEvents() {
        return array(
            FormFlowEvents::PRE_BIND => 'onPreBind',
            FormFlowEvents::POST_VALIDATE => 'onPostValidate',
        );
    }

    /**
     * {@inheritDoc}
     */
    protected function loadStepDescriptions() {
        return array(
            'basics',
            'comment',
            'bug_details',
            'confirmation',
        );
    }

    /**
     * {@inheritDoc}
     */
    public function getFormOptions($formData, $step, array $options = array()) {
        $options = parent::getFormOptions($formData, $step, $options);

        $options['cascade_validation'] = true;

        if ($step > 1) {
            $options['isBugReport'] = $formData->isBugReport();
        }

        return $options;
    }

    /**
     * {@inheritDoc}
     */
    public function reset() {
        parent::reset();
        $this->removeTempIsBugReport();
    }

    /**
     * {@inheritDoc}
     */
    public function createForm($formData, array $options = array()) {
        if ($this->currentStep === 1) {
            $this->removeSkipStep(3);
        }

        return parent::createForm($formData, $options);
    }

    protected function getTempIsBugReportSessionKey() {
        return $this->id . '_isBugReport';
    }

    protected function setTempIsBugReport() {
        $this->storage->set($this->getTempIsBugReportSessionKey(), true);
    }

    protected function isBugReport() {
        return $this->storage->get($this->getTempIsBugReportSessionKey(), false);
    }

    protected function removeTempIsBugReport() {
        $this->storage->remove($this->getTempIsBugReportSessionKey());
    }

    public function onPreBind(PreBindEvent $event) {
        if (!$this->isBugReport()) {
            $this->addSkipStep(3);
        }
    }

    public function onPostValidate(PostValidateEvent $event) {
        $formData = $event->getFormData();

        if (empty($formData)) {
            return;
        }

        if ($this->currentStep >= 1) {
            if ($formData->isBugReport()) {
                $this->setTempIsBugReport();
                $this->removeSkipStep(3);
            } else {
                $this->removeTempIsBugReport();
                $this->addSkipStep(3);
            }
        }
    }

}
<?php

namespace Craue\Symfony2PlaygroundBundle\Form;

use Craue\Symfony2PlaygroundBundle\Entity\Topic;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class CreateTopicForm extends AbstractType {

    /**
     * {@inheritDoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $isBugReport = $options['isBugReport'];

        switch ($options['flowStep']) {
            case 1:
                $builder->add('title');
                $builder->add('description', null, array(
                    'required' => false,
                ));
                $builder->add('category', 'form_type_topicCategory');
                break;
            case 2:
                $builder->add('comment', 'textarea', array(
                    'required' => false,
                ));
                break;
            case 3:
                if ($isBugReport) {
                    $builder->add('details', 'textarea');
                }
                break;
        }
    }

    /**
     * {@inheritDoc}
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver) {
        $resolver->setDefaults(array(
            'flowStep' => 1,
            'data' => new Topic(),
            'isBugReport' => false,
        ));
    }

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

}

Multiple object between steps ?

Hi ,

Is that possible to have, for one given step, another Form Object ?

like

step 1 / 2 / 3 -> CarForm
step 4 / DogForm

Thanks :-)

Dynamically change button names

With regard to the subject, i want to be able to change the step button names according to the step. E.g. In step 1, button.next should be changed and in step 2, another name likewise. Is this possible?

FormFlowBundle for tabbed forms?

Wondering if CraueFormFlowBundle would make sense for complex tabbed forms that don't require an input order. For example, a complex "Contact" form with different tabs/pages for personal and work details.

Or would FormFlow be overkill perhaps?

How can I create the entity after the first step to use its identifier for others steps?

My goal is to have the identifier of my entity after the first step.

I try this:

In my flow:

    public function saveCurrentStepData($id = null) {
        $stepData = $this->retrieveStepData();

        $stepData[$this->currentStep] = $this->request->request->get($this->formType->getName(), array());
        if ($id != null) {
            $stepData[$this->currentStep]['id'] = $id;
        }

        $this->saveStepData($stepData);
    }

In my controller:

    if ($flow->getCurrentStep() == 1 && $user->getId() == null) {
        $em = $this->getDoctrine()->getEntityManager();
        $em->persist($user);
        $em->flush();
    }

    $flow->saveCurrentStepData($user->getId());

I've add $builder->add('id', 'hidden'); too in every steps.

It works except for one case, when I reload the second page (F5).

It works for these cases:
Step 1 -> Step 2 -> Step 3 -> Step 2 -> Step 2(F5)
Step 1 -> Step 2 -> Step 1 -> Step 2 -> Step 2(F5)

It doesn't work for this case:
Step 1 -> Step 2 -> Step 2(F5)
Each time the step 2 is reload with F5, A new identifier is create.

Do you have any idea? I think this is a problem of session.
With my code, the identifier is store in session with the first call of the Step 2, but if we reload the step 2, I suppose the data session is not keep.

How can I make the "BACK" button save form data?

Is there a way to configure things to make the "BACK" button save the form data in a multi-step form?

I have users complaining that the form works fine when they click on "NEXT" but they lose their data when they hit the back button.

Step based data is not available in follow steps

Hi there.

I was trying to use step-based options to add/remove form fields based on the data from a former step.
Dumping $formData in the flow-class revealed that the entity is still the same as in the beginning. Shouldn't this be done?

Step 1 session data being unset

I'm suffering from a very bizarre issue. I have created a three step flow using separate form types. With no validation rules applied I could move between the forms without issue, but once I wrote my validation something strange started happening.

I can submit valid data to step1, seemingly without issue. Step 2 is displayed, and the data from step 1 is in session. When I submit valid data to step 2, I get an exception from one of the validators in step 1, because apparently the data for step 1 was not loaded. What was loaded was an empty array, and an inspection of the session shows that the collection of form data has disappeared. Other data that I have manually entered into session using the Symfony session object is still present however.

After a lot of investigation I decided to put a call to debug-print-backtrace() into Craue\FormFlowBundle\Storage\SessionStorage::remove, because I wanted to track down what was removing my data from the session:

public function remove($key) {
    xdebug_print_function_stack();exit;
    return $this->session->remove($key);
}

Obviously this modification has to be inserted after the initial GET request to step 1, but once done my session data persisted and all three steps of the flow completed successfully.

I have tried this several times, and every time submitting step 2 fails because step 1 data is missing. And as many attempts with the above modification have been successful.

Please can anyone suggest what is going on here?

Custom Flow Transition?

I want to add a custom transition to a form flow. The one I currently require is very simple, but there might be more complex transitions. I'm not even sure, whether this is a good way to accomplish it. I got a flow, which got a step, that allows the user to choose whether to skip a defined part (multiple steps) of the flow.

Flow with custom transition:

class UpdateLeadFormFlow extends AbstractFormFlow
{
    const TRANSITION_SKIP_EDITOR = 'skip_editor';

    protected function loadStepsConfig()
    {
        $fnSkipEditor = function($step, UpdateLeadFormFlow $flow) {
            return UpdateLeadFormFlow::TRANSITION_SKIP_EDITOR === $flow->getRequestedTransition();
        };

        return [
            [
                // The first step, allowing to continue with 2nd or 5th step.
            ],
            [
                // ..
                'skip' => $fnSkipEditor,
            ],
            [
                // ..
                'skip' => $fnSkipEditor,
            ],
            [
                // ..
                'skip' => $fnSkipEditor,
            ],
            [
                // ..
            ],
        ];
    }

    // ...
}

The buttons.html.twig in use:

{% trans_default_domain "leads" %}

{% set prefix = 'update_lead.form_actions.step_' ~ flow.currentStepNumber ~ '.' %}

{% if flow.currentStepNumber == flow.firstStepNumber %}
    <button type="submit" class="btn btn-success btn-large pull-right" name="{{ flow.formTransitionKey }}" value="skip_editor">
        <i class="icon-circle-arrow-right"></i>&nbsp;
        {{- (prefix ~ 'skip')|trans -}}
    </button>
{% endif %}

<button type="submit" class="btn btn-success btn-large pull-right">
    <i class="icon-circle-arrow-right"></i>&nbsp;
    {{- (prefix ~ 'submit')|trans -}}
</button>

{% if flow.currentStepNumber in (flow.firstStepNumber + 1) .. flow.lastStepNumber %}
    <button type="submit" class="btn btn-large pull-right" name="{{ flow.formTransitionKey }}" value="back" formnovalidate>
        <i class="icon-circle-arrow-left"></i>&nbsp;
        {{- (prefix ~ 'back')|trans -}}
    </button>
{% endif %}

The thing I don't like is the unnecessary coupling customization of the template for the custom transition.

I would rather like to have a collection of transitions that are available on a given step. This collection contains the two default transitions already given reset and back. Each step may return a collection of additional transitions to be handled.

If a transition is active, the flow will handle it, e.g. by firing an event to delegate the changes in state. This would also allow to hook into resetting a flow or going backwards.

What are your thoughts? Am I missing some key elements to accomplish what I require? Are transitions the right way?

craueFormFlowBundle and ajax : dynamic form

Hello,

I am having troubles using craueFormFlowBundle combined with an ajax/jquery call to customize the form.
Is that something that can be done ?
I want my radioBox input to be function of the choice of a select, both in the same step.
The error I get is "This form should not contain extra fields". ( i am including the stepField.html.twig file)
How come is that ? I am not adding any new input field inside the form, only replacing input tags with new ones i get from the ajax function, so it should be working.

The problem occurs at step 4, so there is no need to check the other steps.

Here is the code :

<?php

namespace YOP\YourOwnPoetBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use YOP\YourOwnPoetBundle\Entity\MsgCategory;
use YOP\YourOwnPoetBundle\Entity\TraitCategory;
use YOP\YourOwnPoetBundle\Repository\SuperCategoryRepository;
use YOP\YourOwnPoetBundle\Repository\TraitCategoryRepository;

class PoemDataCollectorFormType extends AbstractType {

public function buildForm(FormBuilder $builder, array $options) {
    switch ($options['flowStep']) {
        case 1:
            $builder->add('name');
            $builder->add('sex', 'choice', array(
                'empty_value' => 'Choose sex',
                'choices' => array(1 => 'Male', 2 => 'Female'),
                'label' => 'Receiver\'s sex',
                ));
            $builder->add('userName');
            $builder->add('userSex', 'choice', array(
                'empty_value' => 'Choose sex',
                'choices' => array(1 => 'Male', 2 => 'Female'),
                'label' => 'Specify your sex',
                ));
            break;
        case 2:
            $builder->add('city');
            break;
        case 3:
            $builder->add('relationship', 'entity', array(
                'class' => 'YOPYourOwnPoetBundle:Relationship',
                'empty_value' => 'Choose one',
                'required' => false,
                'query_builder' => function(\Doctrine\ORM\EntityRepository $er)
                {
                    return $er->createQueryBuilder('r')->orderBy('r.type', 'ASC');
                },
                ));
            $builder->add('relationshipFIB','text', array(
                'required' => false,
                'label' => 'or type in yourself:',
            ));
            break;
        case 4:
            $builder->add('traitSuperCategory', 'entity', array(
                'class' => 'YOPYourOwnPoetBundle:SuperCategory',
                'query_builder' => function(\Doctrine\ORM\EntityRepository $er)
                {
                    return $er->findByType(1);
                },
                ));
            $builder->add('traitCategory', 'entity', array(
                'class' => 'YOPYourOwnPoetBundle:TraitCategory',
                'expanded' => true,
                'label' => ' ',
                'query_builder' => function(\Doctrine\ORM\EntityRepository $er)
                {
                    return $er->findBySuperCategoryName('a...');
                },
                ));
            $builder->add('traitFIB','text', array(
                'required' => false,
                'label' => 'YOU create the trait:',
            ));
            break;
        case 5:
            $builder->add('msgCategory', 'entity', array(
                'class' => 'YOPYourOwnPoetBundle:MsgCategory',
                'empty_value' => 'Choose one',
                ));
            $builder->add('msgFIB','text', array(
                'required' => false,
                'label' => 'Input fill in the blank:',
            ));
            $builder->add('age','integer', array(
                'required' => false,
                'label' => 'Input age:',
                ));
            break;
        case 6:
            $builder->add('poemTitle','textarea', array(
                'required' => false,
            ));
            $builder->add('poemClosing', 'textarea', array(
                'required' => false,
            ));
            $builder->add('poemPS', 'textarea', array(
                'required' => false,
            ));
            break;
    }
}


public function getDefaultOptions(array $options)
{
    $options = parent::getDefaultOptions($options);
    $options['flowStep'] = 1;
    $options['data_class'] = 'YOP\YourOwnPoetBundle\PoemBuilder\PoemDataCollector';
    $options['intention'] = 'my_secret_key';

    return $options;
}


public function getName() {
    return 'poemDataCollector';
}
}
?>

collectPoemData.html.twig

{% extends 'YOPYourOwnPoetBundle::layout.html.twig' %}

{% block title %}
    {% if is_granted("IS_AUTHENTICATED_REMEMBERED") %}
        {{ app.user.name }}'s Own Poet
    {% endif %}
    {% if dataCollector.userName is not null %}
        {{ dataCollector.userName }}'s own Poet
    {% endif %}
    {% if not is_granted("IS_AUTHENTICATED_REMEMBERED") and dataCollector.userName is null %}
        Quilly the Poet
    {% endif %}
{% endblock %}

{% block content %}
    <div>
        {{ webpageVerse.webpageLine1 }}
    </div>
    <div>
        {{ webpageVerse.webpageLine2 }}
    </div>
    <div>
        {{ webpageVerse.webpageLine3 }}
    </div>
    <div>
        {{ webpageVerse.webpageLine4 }}
    </div>
    <div>
        {{ webpageVerse.webpageLine5 }}
    </div>
    <br><br><br><br>

    <form method="post" action="{{ path(app.request.attributes.get('_route'),
        app.request.query.all | craue_removeDynamicStepNavigationParameter(flow)) }}" {{ form_enctype(form) }}>
        {% include 'CraueFormFlowBundle:FormFlow:stepField.html.twig' %}
        {% include 'YOPYourOwnPoetBundle:thePoet:collectDataForm.html.twig' %}
        {% include 'CraueFormFlowBundle:FormFlow:buttons.html.twig' %}   
    </form>
{% endblock %}

{% block javascripts %}
    {{ parent() }}
    {% include 'YOPYourOwnPoetBundle:thePoet:collectDataFormScript.html.twig' %}
{% endblock %}

collectDataForm.html.twig

{% if flow.getCurrentStep() == 1 %}
        <div>
            {{ form_label(form.name) }}
            {{ form_widget(form.name) }}
        </div>
        <div id = 'nameError'></div>
        <br>
        <div id = "sexguess"></div>
        <br> <br>
        <div id = "sex">
            {{ form_label(form.sex) }}
            {{ form_widget(form.sex) }}
        </div>
        <div id = "sexError"></div>
        <br><br>
        <div id = 'userName'></div>
        <div id = 'userNameDiv'>
            {{ form_label(form.userName) }}
            {{ form_widget(form.userName) }}
        </div>
        <div id = 'userNameError'></div>
        <br>
        <div id = 'userSexGuess'></div>
        <div id = 'userSexDiv'>
            {{ form_label(form.userSex) }}
            {{ form_widget(form.userSex) }}
        </div>
        <div id = 'userSexError'></div>
        <br> <br>
{% endif %}

{% if flow.getCurrentStep() == 2 %}
        <div>
        {{ form_label(form.city) }}
        {{ form_widget(form.city) }}
        </div>
        <div id = 'cityError'></div>
{% endif %}

{% if flow.getCurrentStep() == 3 %}
        <div id = 'relationship'>
            {{ form_label(form.relationship) }}
            {{ form_widget(form.relationship) }}
        </div>
        <br><br>
        <div id = 'relationshipFIB'>
            {{ form_label(form.relationshipFIB) }}
            {{ form_widget(form.relationshipFIB) }}
        </div>
        <br><br>
        <div id = 'relationshipError'></div>
{% endif %}

{% if flow.getCurrentStep() == 4 %}
        <div>
            {{ form_label(form.traitSuperCategory) }}
            {{ form_widget(form.traitSuperCategory) }}
        </div>

        {{ form_label(form.traitCategory) }}
        {{ form_widget(form.traitCategory) }}

        <div id = 'selectError'></div>
        <div id = FIBDiv>
            {{ form_label(form.traitFIB) }}
            {{ form_widget(form.traitFIB) }}
        </div>
        <div id = FIBError class = "error">
        </div>
{% endif %}

{% if flow.getCurrentStep() == 5 %}
        <div>
        {{ form_label(form.msgCategory) }}
        {{ form_widget(form.msgCategory) }}
        </div>
        <div id = 'selectError'></div>
        <div id = "FIB">
        {{ form_label(form.msgFIB) }}
        {{ form_widget(form.msgFIB) }}
        </div>
        <div id = FIBError class = "error">
        </div>
        <div id = "age">
        {{ form_label(form.age) }}
        {{ form_widget(form.age) }}
        </div>
        <div id = ageError class = "error">
        </div>
{% endif %}

{{ form_rest(form) }}
{{ form_errors(form) }}

collectDataFormScript.html.twig

{# Corrects the title of the page if the user's name ends with letter 's' #}
    {#
    <script>
    var title = new String($("title").text());
    var str = " \'s own Poet";
    var pattern =/(?= own Poet)/g;
    name = title.match(pattern);
    alert(name);
    if( title.charAt( title.length-21 ) == "z") {
        document.title = title+"wouhou";
    }
</script>
    #}
{% if flow.getCurrentStep() == 1 %}
    <script>

        function stripTags(str) {
            return str.replace(/<\/?[^>]+>/gi, '');
        };
        function toTitleCase(str) {
            return str.replace(/(?:^|\s)\w/g, function(match) {
                return match.toUpperCase();
            });
        }

        $(document).ready(function() {

            var selectSex = $("#poemDataCollector_sex");
            var nameField = $("#poemDataCollector_name");
            var sexDiv = $('#sex');
            var sexGuess = $('#sexguess');
            var form = sexDiv.parent();
            var userNameField = $("#poemDataCollector_userName");
            var userSexDiv = $('#userSexDiv');
            var selectUserSex = $('#poemDataCollector_userSex');

            nameField.focus();
            sexDiv.hide();
            sexGuess.hide();
            $('#userNameDiv').hide();
            userSexDiv.hide();


            selectSex.val(0);
            selectUserSex.val(0);

            selectSex.keypress(function(event) {
                if ( event.which == 13 ) {
                    $(".craue_formflow_button_last").click();
                }
            });

            nameField.change(function() {
                $("#nameError").text('');
            });

            userNameField.change(function() {
                $("#userNameError").text('');
            });

            selectSex.change(function() {
                $("#sexError").text('');
            });

            selectUserSex.change(function() {
                $("#userSexError").text('');
            });

            selectUserSex.keypress(function(event) {
                if ( event.which == 13 ) {
                $(".craue_formflow_button_last").click();
                }
            });

            $(".craue_formflow_button_last").click(function(event) {
                event.preventDefault();
                var sex = selectSex.val();
                var name = nameField.val();
                name = stripTags(name);
                name = toTitleCase(name);
                var userName = userNameField.val();
                userName = stripTags(userName);
                userName = toTitleCase(userName);
                var userSex = selectUserSex.val();

                if (name.length < 1){
                    $("#nameError").text("Please enter a name");
                    return false;
                }
                if (sexDiv.is(":visible") && sex == 0) {
                    $("#sexError").text('Please specify '+name+'\'s sex !');
                    return false;
                }

                if (userNameField.is(':visible') && userName.length < 1) {
                    $('#userNameError').text('Please enter your name');
                    return false;
                }

                if (userSexDiv.is(":visible") && userSex == 0) {
                    $("#userSexError").text('Please specify your sex !');
                    return false;
                }

                $.get("{{ path('get_sex_by_name') }}", { receiverName: name, receiverSex: sex, userName: userName, userSex: userSex }, function(data){
                    var result = jQuery.parseJSON(data);

                    if (result.isKnownName == 2)
                        {# Handle user's name #}
                    {
                        if (result.userSex == 1)
                        {
                            selectUserSex.val(1);
                            form.submit();
                        }
                        if (result.userSex == 2)
                        {
                            selectUserSex.val(2);
                            form.submit();
                        }
                        if (result.userSex == 0)
                        {
                            output = userName+' , please tell me your sex, i\'m bad at guessing !';
                            $('#userSexGuess').html(output);
                            userSexDiv.show('slow');
                            userNameField.attr('readonly', 'readonly');
                            selectUserSex.focus();
                            return false;
                        }
                        if (result.userSex == -1)
                        {
                            output = userName+' ?, that\'s an original name !\n I can\'t guess your sex, can you help me ?';
                            $('#userSexGuess').html(output);
                            userSexDiv.show('slow');
                            userNameField.attr('readonly', 'readonly');
                            selectUserSex.focus();
                            return false;
                        }

                    } else {
                        {# Handle receveiver's name #}
                        if (result.sex == 1)
                        {
                            if (result.isKnownName == 0)
                            {
                                selectSex.val(1);
                                selectSex.attr('readonly', 'readonly');
                                $('#userName').html('Hey, since you are not connected, we need to ask for your name !');
                                $('#userNameDiv').show('slow');
                                userNameField.focus();
                                return false;
                            } else {
                                selectSex.val(1);
                                form.submit();
                            }
                        }
                        if (result.sex == 2)
                        {
                            if (result.isKnownName == 0) {
                                selectSex.val(2);
                                selectSex.attr('readonly', 'readonly');
                                $('#userName').html('Hey, since you are not connected, we need to ask for your name !');
                                $('#userNameDiv').show('slow');
                                userNameField.focus();
                                return false;
                            } else {
                                selectSex.val(2);
                                form.submit();
                            }
                        }
                        if (result.sex == 0) 
                        {
                            output = 'Please tell me '+name+' sex, i\'m bad at guessing !';
                            sexGuess.html(output);
                            sexDiv.show('slow');
                            sexGuess.show('slow');
                            nameField.attr('readonly', 'readonly');
                            selectSex.focus();
                            return false;
                        }
                        if (result.sex == - 1)
                        {
                            output = name+' ?, that\'s an original name !\n I can\'t guess its sex, can you help me ?';
                            sexGuess.html(output);
                            sexDiv.show('slow');
                            sexGuess.show('slow');
                            nameField.attr('readonly', 'readonly');
                            selectSex.focus();
                            return false;
                        }
                    }
                });
            });
        });
    </script>
{% endif %}
{% if flow.getCurrentStep() == 2 %}
    <script>
        $(document).ready(function() {
            var cityField = $('#poemDataCollector_city');
            cityField.focus();

            cityField.keypress(function(event) {
                if ( event.which == 13 ) {
                    if (cityField.val().length < 1){
                        $("#cityError").text("Please enter a city");
                        return false;
                    }
                    cityField.parent().parent().submit();
                }
            });
        });
    </script>
{% endif %}
{% if flow.getCurrentStep() == 3 %}
    <script>
        $(document).ready(function() {
            var relationshipError = $('#relationshipError');
            var relationshipSelect = $('#poemDataCollector_relationship');
            relationshipSelect.focus();
            var relationshipFIB = $('#poemDataCollector_relationshipFIB');

            relationshipSelect.keypress(function(event) {
                if ( event.which == 13 ) {
                    if (relationshipSelect.val() == 0) {
                        $('#relationshipError').text('Please select a relationship or type in one yourself');
                        return false;
                    }
                    relationshipSelect.parent().parent().submit();
                }
            });

            $(".craue_formflow_button_last").click(function(){
                if (relationshipSelect.val() == 0 && relationshipFIB.val().length < 1)
                {
                    relationshipError.text('Please choose a relationship from the select box or type in one yourself');
                    return false;
                }
            });

            relationshipSelect.on('change', function() {
                relationshipError.text('');
            });

            relationshipFIB.on('change', function() {
                relationshipError.text('');
            });


        });
    </script>
{% endif %}
{% if flow.getCurrentStep() == 4 %}
    <script>
        function validateFIB(){
            if($("#poemDataCollector_traitFIB").val().length < 1)
            {
                $("#FIBError").text("Please specify the trait");
                return false;
            } else {
                return true;
            }
        }

        function validateTraitSelect() {
            if ( !$("input[name='poemDataCollector[traitCategory]']").is(":checked"))
            {
                $("#selectError").text('Please choose a trait');
                return false;
            } else {
                return true;
            }
        }

        $(document).ready(function() {
            var superCategorySelect = $('#poemDataCollector_traitSuperCategory');
            var FIBDiv = $("#FIBDiv");
            var TraitCategoryDiv = $("#traitCategoryDiv");
            var traitCtgCheckbox = $('#poemDataCollector_traitCategory');
            FIBDiv.hide();

            superCategorySelect.focus();

            $('body').keypress(function(event) {
                if ( event.which == 13 ) {
                    if (FIBDiv.is(":visible")) {
                        if (!validateFIB())
                            return false;
                    } else {
                        if (!validateTraitSelect())
                            return false;
                    }
                    superCategorySelect.parent().parent().submit();
                }
            });

            superCategorySelect.on('change', function() {
                $("#poemDataCollector_traitFIB").val('');
                $("#FIBError").text('');
                $("#selectError").text('');


                var selectedSP = $(this).val();

                $.ajax({
                    url: '{{ path('get_trait_categories') }}',
                    data: 'superCategoryID='+selectedSP,
                    dataType: 'json' ,
                    success: function(json) {

                        traitCtgCheckbox.empty();
                        $.each(json, function(index, value) {
                            var inp = "<input id='poemDataCollector_traitCategory_"+index+"' type='radio' value='"+index+"' required='required' name='poemDataCollector[traitCategory]'>";
                            var lbl = "<label class=' required' for='poemDataCollector_traitCategory_"+index+"'>"+value+"</label>";
                            traitCtgCheckbox.append(inp).append(lbl + '&nbsp;&nbsp;');
                        });
                    }
                });

                if ( $('select option:selected').text() == 'YOU create the trait...')
                {
                    FIBDiv.show('slow');
                } else {
                    FIBDiv.hide('slow');
                }
            });

            $("input[name='poemDataCollector[traitCategory]']").live('change', function() {
                $("#selectError").text('');
                checkedBoxLabel = $('input[name=poemDataCollector[traitCategory]]:checked + label').text();
                alert(checkedBoxLabel);
                if (checkedBoxLabel.indexOf('___') != -1)
                {
                    FIBDiv.show('slow');
                } else {
                    FIBDiv.hide('slow');
                }
            });

            $(".craue_formflow_button_last").click(function(){
                if ( $('select option:selected').text() == 'YOU create the trait...' )
                    return validateFIB();
                else
                    return validateTraitSelect();
            });

            $("#poemDataCollector_traitFIB").on('change', function() {
                $("#FIBError").text("");
            });
        });
    </script>
{% endif %}

{% if flow.getCurrentStep() == 5 %}
    <script>
        function validateFIB(){
            //if it's NOT valid
            if($("#poemDataCollector_msgFIB").val().length < 1){
                $("#FIBError").text("Please specify your message");
                return false;
            }
            //if it's valid
            else{
                return true;
            }
        }

        function validateAge() {
            //if not valid
            if($("#poemDataCollector_age").val().length < 1) {
                $("#ageError").text("Please specify the age");
                return false;
            } else {
                return true;
            }
        }

        function validateMsgSelect() {
            if ( !$("input[name='poemDataCollector[msgCategory]']").is(":checked"))
            {
                $("#selectError").text('Please choose a message');
                return false;
            } else {
                return true;
            }
        }

        $(document).ready(function() {
            var select = $("#poemDataCollector_msgCategory");

            if ( (select.val() < 2) ||  (select.val() > 13))
                $("#FIB").hide();
            if ( select.val() != 1)
                $("#age").hide();


            select.focus();

            $('body').keypress(function(event) {
                $("#FIBError").text("");
                if ( (select.val() > 1) &&  (select.val() < 14))
                {
                    $("#FIB").show('slow');
                    $("#poemDataCollector_msgFIB").focus();

                } else {
                    $("#FIB").hide('slow');
                }
                if  (select.val() == 1 )
                {
                    $("#age").show('slow');
                    $("#poemDataCollector_age").focus();
                } else {
                    $("#age").hide('slow');
                }
                if ( event.which == 13 ) {
                    if ( select.val() == 0 ) {
                        $("#selectError").text("Please select a message");
                        return false;
                    }
                    if ($("#FIB").is(":visible")) {
                        if (!validateFIB())
                            return false;
                    }
                    if ($("#age").is(":visible")) {
                        if (!validateAge())
                            return false;
                    }
                    select.parent().parent().submit();
                }
            });

            select.change(function() {
                $("#poemDataCollector_msgFIB").val('');
                $("#poemDataCollector_age").val('');
                if ( (select.val() > 1) &&  (select.val() < 14))
                {
                    $("#FIB").show('slow');
                    $("#poemDataCollector_msgFIB").focus();
                } else {
                    $("#FIB").hide('slow');
                }
                if  (select.val() == 1 )
                {
                    $("#age").show('slow');
                    $("#poemDataCollector_age").focus();
                } else {
                    $("#age").hide('slow');
                }
            });

            $(".craue_formflow_button_last").click(function(){
                if ( (select.val() > 1) &&  (select.val() < 14) )
                    return validateFIB();
                if ( select.val() == 1 )
                    return validateAge();
            });

            select.focus(function() {
                $("#FIBError").text("");
                $("#ageError").text("");
            });

            $("#poemDataCollector_msgFIB").focus(function() {
                $("#FIBError").text("");
                $("#ageError").text("");
            });

        }); 
    </script>
{% endif %}

And the controller :

public function collectPoemDataAction() {

    /* Collector collects all the data inputs */

    $collector = $this->get('yop.poem.datacollector');
    $flow = $this->get('yop.form.flow.poemDataCollector');
    $flow->bind($collector);

    $webpageVerseSelector = $this->get('yop.poem.webpageVerseSelector');

    $customizer = $this->get('yop.poem.poemcustomizer');

    $form = $flow->createForm($collector);

    if ($flow->isValid($form)) {
        $flow->saveCurrentStepData();

        echo 'en haut <br>';
        echo "current step : ".$flow->getCurrentStep().'<br>';
        echo "requested transition : ".$flow->getRequestedTransition().'<br>';
        echo "requested step : ".$flow->getRequestedStep().'<br>';
        echo 'determine current step : '.$flow->determineCurrentStep().'<br>';

        if ($flow->nextStep()) {

            var_dump($form->getErrors());

            echo 'en haut après nextStep() <br>';
            echo "current step : ".$flow->getCurrentStep().'<br>';
            echo "requested transition : ".$flow->getRequestedTransition().'<br>';
            echo "requested step : ".$flow->getRequestedStep().'<br>';
            echo 'determine current step : '.$flow->determineCurrentStep().'<br>';

            if (!$flow->isStepDone(5)) {
                $webpageVerseSelector->selectWebpageVerse($flow->getCurrentStep());
                $customizedWebpageVerse = new CustomizedWebpageVerse();
                $customizedWebpageVerse = $customizer->customizePoem($customizedWebpageVerse, $webpageVerseSelector);

                return $this->render('YOPYourOwnPoetBundle:thePoet:collectPoemData.html.twig',array(
                    'form' => $flow->createForm($collector)->createView(),
                    'flow' => $flow,
                    'webpageVerse' => $customizedWebpageVerse,
                    'dataCollector' => $collector,
                ));
            }else {
                $flow->setCurrentStep(6);
                if ( $collector->getTraitCategory()->getFillInBlank())
                {
                    $stringTool = new StringTools();
                    $collector->setTraitCtgWithFIBName($stringTool->buildCtgName($collector->getTraitCategory()->getName(), $collector->getTraitFIB()));
                    unset($stringTool);
                }
                if ( $collector->getMsgCategory()->getFillInBlank())
                {
                    $stringTool = new StringTools();
                    $collector->setMsgCtgWithFIBName($stringTool->buildCtgName($collector->getMsgCategory()->getName(), $collector->getMsgFIB()), $this->getDoctrine());
                    unset($stringTool);
                }
                if ( $collector->getMsgCategory()->getAgeFIB())
                {
                    $stringTool = new StringTools();
                    $ageObject = $this->getDoctrine()
                        ->getRepository('YOPYourOwnPoetBundle:AgeDictionary')
                        ->find($collector->getAge());
                    $collector->setMsgCtgWithAgeName($stringTool->buildCtgName($collector->getMsgCategory()->getName(), $ageObject->getAdjectiveString()));
                    unset($stringTool);
                    unset ($ageObject);
                }
                /* if user is connected, we have not collected sender name and sex,
                 * we have to get it from the user
                 */
                $user = $this->container->get('security.context')->getToken()->getUser();
                if ($user instanceof User AND is_object($user))
                {
                    $collector->setUserName($user->getName());
                    $collector->setUserSex($user->getSex());
                }
                /* before reviewing poem, set poem title, closing and ps in collector */
                $collector->setPoemTitle($customizer->getCustomizedTitle());
                $collector->setPoemClosing($customizer->getCustomizedClosing());
                $collector->setPoemPS($customizer->getCustomizedPS());
                return $this->render('YOPYourOwnPoetBundle:thePoet:reviewPoem.html.twig',array(
                    'form' => $flow->createForm($collector)->createView(),
                    'flow' => $flow,
                    'dataCollector' => $collector,
                ));
            }
        } else {

            /* collector calculates the number of syllables
            * in some values collected : name, city and relationship
            */
            $collector->calculateSlb();

            /* selector selects which raw verses will be used */
            $selector = $this->get('yop.poem.verseSelector');

            /* check in cookies if have a last verse for the category we want
             * set selector if we find cookies
             */
            $request = $this->get('request');
            if ($request->cookies->has('lastIntroID')) {
                $introVerse = $this->getDoctrine()
                        ->getRepository('YOPYourOwnPoetBundle:IntroVerse')
                        ->find($request->cookies->get('lastIntroID'));
                $selector->setIntroVerse($introVerse);
            }
            if ($request->cookies->has('lastMsg'.str_replace(" ", "",$collector->getMsgCategory()->getName()).'ID')) {
                $messageVerse = $this->getDoctrine()
                        ->getRepository('YOPYourOwnPoetBundle:MessageVerse')
                        ->find($request->cookies->get('lastMsg'.str_replace(" ", "",$collector->getMsgCategory()->getName()).'ID'));
                $selector->setMessageVerse($messageVerse);
            }
            if ($request->cookies->has('lastTrait'.str_replace(" ", "",$collector->getTraitCategory()->getName()).'ID')) {
                $traitVerse = $this->getDoctrine()
                        ->getRepository('YOPYourOwnPoetBundle:TraitVerse')
                        ->find($request->cookies->get('lastTrait'.str_replace(" ", "",$collector->getTraitCategory()->getName()).'ID'));
                $selector->setTraitVerse($traitVerse);
            }
            /* select verses for the poem (and test if everything goes well */
            if ($selector->selectVerses() == NULL)
                throw new \Exception('Quilly was unable to create this poem for you...');

            /* customizer customizes the raw poem with data
            * input by the visitor to make the real poem
            */
            $customizedPoem = new CustomizedPoem();
            $customizedPoem = $customizer->customizePoem($customizedPoem);

            /* store data collected from user in session
             * to be able to reuse after page change
             */
            $session = $request->getSession();
            $session->set('name', $collector->getName());
            $session->set('nameNbSlb', $collector->getNameNbSlb());
            $session->set('sex', $collector->getSex());
            $session->set('city', $collector->getCity());
            $session->set('cityNbSlb', $collector->getCityNbSlb());
            $session->set('relationship', $collector->getRelationship());
            $session->set('relationshipFIB', $collector->getRelationshipFIB());
            $session->set('relationshipNbSlb', $collector->getRelationshipNbSlb());
            $session->set('traitID', $collector->getTraitCategory()->getId());
            //$session->set('trait', $collector->getTraitCategory());
            if ($collector->getTraitFIB()) {
                $session->set('traitFIB', $collector->getTraitFIB());
            }
            //$session->set('message', $collector->getMsgCategory());
            $session->set('messageID', $collector->getMsgCategory()->getId());
            if ($collector->getMsgFIB()) {
                $session->set('msgFIB', $collector->getMsgFIB());
            }
            if ($collector->getAge()) {
                $session->set('msgAge', $collector->getAge());
            }

            /* store last verses seen by user in cookies */
            $response = new Response();
            $stringTool = new StringTools();
            $response->headers->setCookie(new Cookie('lastIntroID', $selector->getIntroVerse()->getID(), time() + 3600 * 24 * 7));
            $response->headers->setCookie(new Cookie('lastMsg'.$stringTool->cookieRename($collector->getMsgCategory()->getName()).'ID', $selector->getMessageVerse()->getID(), time() + 3600 * 24 * 7));
            $response->headers->setCookie(new Cookie('lastTrait'.$stringTool->cookieRename($collector->getTraitCategory()->getName()).'ID', $selector->getTraitVerse()->getID(), time() + 3600 * 24 * 7));
            $response->send();
            unset($stringTool);

            $flow->reset();

            $changeIntroLink = $this->get('router')->generate('poem_change_intro');
            $changeMessageLink = $this->get('router')->generate('poem_change_message');
            $changeTraitLink = $this->get('router')->generate('poem_change_trait');

            return $this->render('YOPYourOwnPoetBundle:thePoet:showPoem.html.twig',array(
                'poem' => $customizedPoem,
                'dataCollector' => $collector,
                'changeIntroVerseLink' => $changeIntroLink,
                'changeMessageVerseLink' => $changeMessageLink,
                'changeTraitVerseLink' => $changeTraitLink,
                )); 
        }
    }

    var_dump($form->getErrors());

    echo 'en bas <br>';
    echo "current step : ".$flow->getCurrentStep().'<br>';
    echo "requested transition : ".$flow->getRequestedTransition().'<br>';
    echo "requested step : ".$flow->getRequestedStep().'<br>';
    echo 'determine current step : '.$flow->determineCurrentStep().'<br>';

    if ( $flow->getRequestedTransition() == 'reset' OR !$flow->isStepDone(1))
        /* start data collection process from start again */
    {

        $webpageVerseSelector->selectWebpageVerse(1);
        $customizedWebpageVerse = new CustomizedWebpageVerse();
        $customizedWebpageVerse = $customizer->customizePoem($customizedWebpageVerse, $webpageVerseSelector);
        return $this->render('YOPYourOwnPoetBundle:thePoet:collectPoemData.html.twig',array(
            'form' => $flow->createForm($collector)->createView(),
            'flow' => $flow,
            'webpageVerse' => $customizedWebpageVerse,
            'dataCollector' => $collector,
        ));
    }

    if ($flow->isStepDone(5) && $flow->determineCurrentStep() < 6) {
        return $this->render('YOPYourOwnPoetBundle:thePoet:modifyPoemData.html.twig',array(
            'form' => $flow->createForm($collector)->createView(),
            'flow' => $flow,
            'dataCollector' => $collector,
        ));
    }
    if ($flow->determineCurrentStep() == 6) {
        return $this->render('YOPYourOwnPoetBundle:thePoet:reviewPoem.html.twig',array(
            'form' => $flow->createForm($collector)->createView(),
            'flow' => $flow,
            'dataCollector' => $collector,
        ));
    }

    $flow->reset();
    /* if commands arrive here, a problem has occured, throw error */
    throw new \Exception('Quilly got confused.');
}

"This form should not contain extra fields" on step 1 (Symfony2.1)

I'm getting this error constantly, and having tried disabling CSRF protection and removing all of my more complicated FormType's, I'm at somewhat of a loss as to what is causing the problem.

Note: I tried removing the event listener, no change.

My FormType looks something like this:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    switch ($options['flowStep']) {
        case 1:

            $builder->addEventListener(FormEvents::PRE_SET_DATA, function($event) use ($builder) {
                $form = $event->getForm();
                $review = $event->getData();

                $factory = $builder->getFormFactory();

                if (!$review->getDealership()) {
                    $field = $factory->createNamed('dealership', 'dealership');
                } else {
                    $field = $factory->createNamed('dealership', 'hidden', $review->getDealership()->getId());
                }

                $form->add($field);
            });

            $builder->add('ratings', 'collection', array(
                'type' => new Rating,
            ));

            $builder->add('comments');

            break;

        case 2:

            $builder->add('department', 'department');

            // $builder->add('employees');

            $builder->add('visited', null, array(
                'label' => 'Date of visited to dealership',
                'format' => 'dd-MM-yyyy',
                'years' => range(date('Y'), date('Y') - 2)
            ));;

            break;
    }
}

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'ACMEDemo\ReviewBundle\Entity\Review',
        'flowStep' => 1
    ));
}

and my template features:

{% include 'CraueFormFlowBundle:FormFlow:stepField.html.twig' %}

Any thoughts on what the problem might be?

Also, when first loading the form, getCurrentStep returns null and I get a blank form, I have to click next to get to step 1, despite my setDefaultOptions looking like:

    $resolver->setDefaults(array(
        'data_class' => 'ACMEDemo\ReviewBundle\Entity\Review',
        'flowStep' => 1
    ));

Any ideas?

Use Same Entity on Multiple Steps?

Is it possible to populate a single entities fields throughout multiple steps? I found when I tried to use the same entity on 2 steps, the entities values that got set on the first step got set to NULL once the second step was submitted.

Validator callback isn't called ?

Hi,

I needed a particular field to be check with my own logic. So I created a Validator Callback.
I was wondering why my validator callback is not called using form flow :

<?php 

 * @ORM\Table(name="team")
 * @ORM\Entity(repositoryClass="...\Entity\TeamRepository")
 * @Gedmo\SoftDeleteable(fieldName="deletedAt")
 * @Assert\Callback(methods={"isLevelValid"})
Class Team
{
    public function isLevelValid(ExecutionContext $context)
    {
        die('not dying ?');
    }
}

If you got any clues,
Thanks :-)

Documentation Issue

Hallo.

I just found your bundle and like it alot. While setting up my first flow-form I stumbled over a flaw in the documentation:

In the section "Create an action" the last four lines read:
return array(
'form' => $form->createView(),
'flow' => $flow,
);

but should be:
return $this->render('StatusFaultBundle:Fault:new.html.twig', array(
'form' => $form->createView(),
'flow' => $flow,
));

Custom functions in form class

How can i write my own function inside a form that i have extended the AbstractType ? I wrote a function but i cannot access it. Gives an error saying its an undefined method.

Encapsulating the flow inside a form handler is not possible!?

Hi there.

I wanted to encapsulate my flow handling into an form handler to remove duplicate code I had in my controller.
Unfortunately I was not successful but got strange errors.

The formhandler consists only of the flow, entity manager and router. All injected via the constructor from the DIC. The logic from the controller is capsuled in an method dealing with all the stuff like binding, validating etc.

But it doesn't work. I always end up having errors after submitting the first step:

Method "titles" for object "Symfony\Component\Form\FormView" does not exist in FmdbReleaseBundle:Release:form.html.twig at line 34

Copying the logic back to the controller everything works fine.

Has anyone ever done something like this before?

Marcus

Validation is skipped

Hi,

I finally figured out what was causing the missing validation of some of our forms, it's the FormFlow, more specifically:

<?php
    public function getFormOptions($formData, $step, array $options = array()) {
        $options['flowStep'] = $step;

        if (!array_key_exists('validation_groups', $options)) {
            $options['validation_groups'] = $this->validationGroupPrefix . $step;
        }

        return $options;
    }

If there is no validation_groups set (or passed when creating the form via the flow), one will be added, which in return disables validation of used form types in the form of the flow!

Document 2.0 changes and upgrade instructions

What's new? Are there performance enhancements and new features, or mainly just semantic changes?

These would be helpful in deciding if it's worth the upgrade. Upgrade help content would also be great.

Question: Saving a FormFlow

This probably isn't useful for everyone so this isn't a feature request. Just a question for @craue -

If you were to save a model from a form flow for later opening - how would you go about populating step data from that saved model? Since step data is stored in the format it comes through as in the request, I'm not sure how to recreate this data from the model.

Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.