New in Symfony 8.2 (in development). A range (two dates, two prices, two sizes) used to be declared as two fields, and the rule that ties them together had nowhere to live: it ended up in a POST_SUBMIT listener copied from one form to the next. BoundsType renders both bounds with the same type and takes care of the comparison.
Before
$builder ->add('from', DateType::class, ['widget' => 'single_text', 'label' => 'From']) ->add('to', DateType::class, ['widget' => 'single_text', 'label' => 'To']) ->addEventListener(FormEvents::POST_SUBMIT, static function (FormEvent $event): void { // compare the two dates, put the error on the right one… });
Since Symfony 8.2
$builder->add('period', BoundsType::class, [ 'type' => DateType::class, 'options' => ['widget' => 'single_text'], 'from_options' => ['label' => 'From'], 'to_options' => ['label' => 'To'], 'compare' => true, 'compare_message' => 'The start date must come before the end date.', ]);
The field has two children, from and to, rendered by the type passed in type. options applies to both; from_options and to_options are merged on top for what sets them apart. Like any compound field, it has its own label, guessed from its name: label: false removes it. compare: true checks the order of the bounds and reports the error on the lower bound, the one that must move; dates, scalars and enums are ordered on their own, any other kind of bound needs a callable.
An empty bound is still allowed: compare only deals with closed ranges, and the field returns null (not ['from' => null, 'to' => null]) when both bounds are empty.
Your task
In RechercheType, replace the two date fields with a single period field, and let BoundsType compare the bounds: the listener no longer has a reason to exist. Update EmpruntsController, which now reads both bounds from the field's data. The /loans page must behave as before, labels and error message included.