fix: enforce mutability checks for all fields

This commit is contained in:
Björn Fromme
2026-02-24 11:53:00 +01:00
parent 31153e90b4
commit 5a64c85fb6
3 changed files with 67 additions and 3 deletions
@@ -21,6 +21,32 @@ use App\Form\Service\Contract\ParticipantFieldHandlerInterface;
*/
class ParticipantFieldHandlerRegistry
{
private const MUTABILITY_ADDITIONAL_SERVICES = 'additional_services';
private const MUTABILITY_TRANSPORTATION = 'transportation';
private const MUTABILITY_PICKUPS = 'pickups';
/**
* Maps handler names to mutability categories for edit-mode enforcement.
*
* @var array<string, string>
*/
private const HANDLER_MUTABILITY_CATEGORY_MAP = [
'additionalServices' => self::MUTABILITY_ADDITIONAL_SERVICES,
'courses' => self::MUTABILITY_ADDITIONAL_SERVICES,
'board' => self::MUTABILITY_ADDITIONAL_SERVICES,
'veg' => self::MUTABILITY_ADDITIONAL_SERVICES,
'skiPass' => self::MUTABILITY_ADDITIONAL_SERVICES,
'rentals' => self::MUTABILITY_ADDITIONAL_SERVICES,
'rentalInsurance' => self::MUTABILITY_ADDITIONAL_SERVICES,
'transportationOutbound' => self::MUTABILITY_TRANSPORTATION,
'transportationInbound' => self::MUTABILITY_TRANSPORTATION,
'parking' => self::MUTABILITY_TRANSPORTATION,
'licensePlate' => self::MUTABILITY_TRANSPORTATION,
'transportationDiscountReplacement' => self::MUTABILITY_TRANSPORTATION,
'pickup' => self::MUTABILITY_PICKUPS,
'dropOff' => self::MUTABILITY_PICKUPS,
];
/** @var array<string, ParticipantFieldHandlerInterface> Registered handlers indexed by field name */
private array $handlers = [];
@@ -121,6 +147,10 @@ class ParticipantFieldHandlerRegistry
// Process each handler across all participants before moving to the next handler
// This ensures booking-level state (like family booking detection) is accurate
foreach ($sortedHandlerNames as $handlerName) {
if (false === $this->isHandlerMutableForBooking($handlerName, $bookingDto)) {
continue;
}
$handler = $this->handlers[$handlerName];
// Apply this handler to all participants
@@ -158,6 +188,10 @@ class ParticipantFieldHandlerRegistry
// Apply each handler to the specified participant
foreach ($sortedHandlerNames as $handlerName) {
if (false === $this->isHandlerMutableForBooking($handlerName, $bookingDto)) {
continue;
}
$handler = $this->handlers[$handlerName];
// Let each handler decide if it should process this participant's data
@@ -224,8 +258,11 @@ class ParticipantFieldHandlerRegistry
'courses',
'additionalServices',
'board',
'veg',
'pickup',
'dropOff',
'parking',
'licensePlate',
];
foreach ($serviceFields as $fieldName) {
@@ -521,6 +558,25 @@ class ParticipantFieldHandlerRegistry
return $this->convertSingleValueToSubmittedFormat($dtoValue);
}
/**
* Checks whether a field handler is allowed to run based on edit-mode mutability flags.
*/
private function isHandlerMutableForBooking(string $handlerName, BookingDto $bookingDto): bool
{
if (BookingDto::MODE_CREATE === $bookingDto->getMode()) {
return true;
}
$mutabilityCategory = self::HANDLER_MUTABILITY_CATEGORY_MAP[$handlerName] ?? null;
return match ($mutabilityCategory) {
self::MUTABILITY_ADDITIONAL_SERVICES => $bookingDto->travel->additionalServicesMutable,
self::MUTABILITY_TRANSPORTATION => $bookingDto->travel->transportationServicesMutable,
self::MUTABILITY_PICKUPS => $bookingDto->travel->pickupsMutable,
default => true,
};
}
/**
* Converts a single DTO value to submitted form format.
*
+5 -1
View File
@@ -41,7 +41,8 @@
{%- for child in form %}
{% set choiceData = form.vars.choices[loop.index0] is defined ? form.vars.choices[loop.index0].data : null %}
{% set description = child.vars.attr['data-description']|default(null) %}
{% set isReadonly = child.vars.attr.readonly is defined %}
{% set fieldReadonly = form.vars.attr.readonly is defined %}
{% set isReadonly = fieldReadonly or child.vars.attr.readonly is defined %}
{%- set child_attr = {} -%}
{%- if attr['hx-trigger'] is defined -%}
{%- set child_attr = {
@@ -51,6 +52,9 @@
'hx-swap': attr['hx-swap']
} -%}
{%- endif -%}
{%- if fieldReadonly and child.vars.attr.readonly is not defined -%}
{%- set child_attr = child_attr|merge({'readonly': true}) -%}
{%- endif -%}
<tr>
<td class="border border-primary-bg p-2 align-top">
<div>{{- child.vars.label -}}</div>
+6 -2
View File
@@ -205,7 +205,9 @@
<div{% if tooltip is not null %} {{ stimulus_controller('tooltip', { 'content': tooltip }) }}{% endif %}>
{%- if attr.readonly is defined %}
<div class="cursor-not-allowed">
{{ parent() }}
<div class="pointer-events-none">
{{ parent() }}
</div>
</div>
{% else %}
{{ parent() }}
@@ -223,6 +225,9 @@
'hx-swap': attr['hx-swap']
}) -%}
{%- endif -%}
{%- if attr.readonly is defined and child.vars.attr.readonly is not defined -%}
{%- set child_attr = child_attr|merge({'readonly': true}) -%}
{%- endif -%}
{%- if child.vars.attr['data-tooltip'] is defined -%}
{%- set child_attr = child_attr|merge({'data-tooltip': child.vars.attr['data-tooltip']}) -%}
{%- endif -%}
@@ -358,4 +363,3 @@
</div>
</div>
{% endblock %}