Installation:
composer require craue/formflow-bundle
Enable the bundle in config/bundles.php (Symfony Flex) or AppKernel.php (legacy).
First Use Case:
Create a form flow controller extending Craue\FormFlowBundle\Controller\FormFlowController.
Example:
use Craue\FormFlowBundle\Controller\FormFlowController;
use Symfony\Component\HttpFoundation\Request;
class VehicleFlowController extends FormFlowController
{
public function createVehicleFlowAction(Request $request)
{
return $this->handleFlow(
'vehicle_flow', // Unique flow name
$request,
'App\Form\VehicleType' // Your form type
);
}
}
Routing: Add a route targeting the controller method:
# config/routes.yaml
vehicle_flow:
path: /vehicle/create
controller: App\Controller\VehicleFlowController::createVehicleFlowAction
Form Type:
Define a form type with steps (e.g., VehicleType):
use Craue\FormFlowBundle\Form\Flow\FormInterface;
use Craue\FormFlowBundle\Form\Flow\FormStepInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class VehicleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('make', TextType::class)
->add('model', TextType::class)
->add('year', IntegerType::class);
}
public function getSteps(FormInterface $flow): array
{
return [
'basic_info' => [
'fields' => ['make', 'model'],
'label' => 'Basic Info',
],
'details' => [
'fields' => ['year'],
'label' => 'Details',
],
];
}
}
Template:
Use the built-in template (formflow.html.twig) or extend it:
{% extends 'formflow.html.twig' %}
{% block formflow_steps %}
{{ parent() }}
<!-- Custom step navigation or logic -->
{% endblock %}
Step Definition:
getSteps() in your form type to define steps with:
fields: Array of field names for the step.label: Human-readable step name.validation_groups, skipable, redirect_route.public function getSteps(FormInterface $flow): array
{
return [
'step1' => [
'fields' => ['field1', 'field2'],
'label' => 'Step 1',
'validation_groups' => ['step1'],
],
'step2' => [
'fields' => ['field3'],
'label' => 'Step 2',
'skipable' => true,
],
];
}
Dynamic Navigation:
getSteps() dynamically based on user input or data:
public function getSteps(FormInterface $flow): array
{
$data = $flow->getData();
if ($data->isPremiumUser()) {
return ['premium_step' => [...]];
}
return ['basic_step' => [...]];
}
Validation Groups:
# config/validation.yaml
App\Entity\Vehicle:
constraints:
- Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: { fields: make, message: 'Make already exists.' }
groups:
step1:
- NotBlank: { groups: [step1], message: 'Make is required.' }
step2:
- Range: { groups: [step2], min: 1900, max: date('Y') }
File Uploads:
@Assert\File constraints:
$builder->add('document', FileType::class, [
'label' => 'Upload Document',
'mapped' => false, // Or true if storing in DB
'constraints' => [
new File(['maxSize' => '1024k', 'mimeTypes' => ['application/pdf']])
],
]);
Post/Redirect/Get (PRG):
$this->handleFlow('flow_name', $request, 'App\Form\Type', [
'prg' => true,
'prg_route' => 'vehicle_flow_success',
]);
Doctrine Integration:
Bind the form to an entity (e.g., Vehicle) and persist after submission:
public function createVehicleFlowAction(Request $request, EntityManagerInterface $em)
{
return $this->handleFlow('vehicle_flow', $request, 'App\Form\VehicleType', [
'data_class' => Vehicle::class,
'on_success' => function ($flow, $data) use ($em) {
$em->persist($data);
$em->flush();
},
]);
}
Event Listeners:
Subscribe to flow events (e.g., FormFlowEvent) for custom logic:
use Craue\FormFlowBundle\Event\FormFlowEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class FlowSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
FormFlowEvent::PRE_SUBMIT => 'onPreSubmit',
];
}
public function onPreSubmit(FormFlowEvent $event)
{
$flow = $event->getFlow();
if ($flow->getCurrentStep() === 'step1') {
$flow->setDataClass('App\Entity\VehicleDraft');
}
}
}
Twig Extensions: Access flow data in templates:
{{ formflow.getCurrentStep() }} {# Outputs current step name #}
{{ formflow.getStepLabel('step1') }} {# Outputs step label #}
Step Field Mismatch:
getSteps() returns fields not present in the form, the flow will throw an exception.fields in getSteps() match the form builder’s field names exactly.Data Binding Issues:
data_class), ensure all steps’ fields are mapped to entity properties.mapped => false for non-entity fields or add them to the entity.Validation Groups Not Triggering:
validation_groups in getSteps() match those in validation.yaml.{{ dump(form.errors) }} in Twig.PRG Redirect Loops:
prg_route points to a valid route and handle the route’s controller logic.File Uploads and Steps:
PRE_SUBMIT if needed:
$event->getForm()->remove('file_field');
Log Flow State:
Use the FormFlowEvent to log the current step or data:
public function onPreSubmit(FormFlowEvent $event)
{
$this->logger->debug('Current step:', ['step' => $event->getFlow()->getCurrentStep()]);
}
Check Flow Data: Dump the flow’s data object in a template:
{{ dump(formflow.getData()) }}
Validate Step Configuration:
Ensure getSteps() returns an associative array with step names as keys:
// Correct:
return ['step1' => [...], 'step2' => [...]];
// Incorrect (will fail):
return [[...], [...]];
Custom Step Templates:
Override the default step template (formflow/steps.html.twig) to customize rendering:
{% extends 'formflow/steps.html.twig' %}
{% block step_label %}
<h2 class="custom-step-label">{{ label }}</h2>
{% endblock %}
Dynamic Step Addition:
Add steps dynamically via FormFlowEvent::PRE_GET_STEPS:
public function onPreGetSteps(FormFlowEvent $event)
{
$steps = $event->getSteps();
How can I help you explore Laravel packages today?