fix: enforce mutability checks for all fields
This commit is contained in:
@@ -21,6 +21,32 @@ use App\Form\Service\Contract\ParticipantFieldHandlerInterface;
|
|||||||
*/
|
*/
|
||||||
class ParticipantFieldHandlerRegistry
|
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 */
|
/** @var array<string, ParticipantFieldHandlerInterface> Registered handlers indexed by field name */
|
||||||
private array $handlers = [];
|
private array $handlers = [];
|
||||||
|
|
||||||
@@ -121,6 +147,10 @@ class ParticipantFieldHandlerRegistry
|
|||||||
// Process each handler across all participants before moving to the next handler
|
// Process each handler across all participants before moving to the next handler
|
||||||
// This ensures booking-level state (like family booking detection) is accurate
|
// This ensures booking-level state (like family booking detection) is accurate
|
||||||
foreach ($sortedHandlerNames as $handlerName) {
|
foreach ($sortedHandlerNames as $handlerName) {
|
||||||
|
if (false === $this->isHandlerMutableForBooking($handlerName, $bookingDto)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$handler = $this->handlers[$handlerName];
|
$handler = $this->handlers[$handlerName];
|
||||||
|
|
||||||
// Apply this handler to all participants
|
// Apply this handler to all participants
|
||||||
@@ -158,6 +188,10 @@ class ParticipantFieldHandlerRegistry
|
|||||||
|
|
||||||
// Apply each handler to the specified participant
|
// Apply each handler to the specified participant
|
||||||
foreach ($sortedHandlerNames as $handlerName) {
|
foreach ($sortedHandlerNames as $handlerName) {
|
||||||
|
if (false === $this->isHandlerMutableForBooking($handlerName, $bookingDto)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$handler = $this->handlers[$handlerName];
|
$handler = $this->handlers[$handlerName];
|
||||||
|
|
||||||
// Let each handler decide if it should process this participant's data
|
// Let each handler decide if it should process this participant's data
|
||||||
@@ -224,8 +258,11 @@ class ParticipantFieldHandlerRegistry
|
|||||||
'courses',
|
'courses',
|
||||||
'additionalServices',
|
'additionalServices',
|
||||||
'board',
|
'board',
|
||||||
|
'veg',
|
||||||
'pickup',
|
'pickup',
|
||||||
'dropOff',
|
'dropOff',
|
||||||
|
'parking',
|
||||||
|
'licensePlate',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($serviceFields as $fieldName) {
|
foreach ($serviceFields as $fieldName) {
|
||||||
@@ -521,6 +558,25 @@ class ParticipantFieldHandlerRegistry
|
|||||||
return $this->convertSingleValueToSubmittedFormat($dtoValue);
|
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.
|
* Converts a single DTO value to submitted form format.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -41,7 +41,8 @@
|
|||||||
{%- for child in form %}
|
{%- for child in form %}
|
||||||
{% set choiceData = form.vars.choices[loop.index0] is defined ? form.vars.choices[loop.index0].data : null %}
|
{% 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 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 = {} -%}
|
{%- set child_attr = {} -%}
|
||||||
{%- if attr['hx-trigger'] is defined -%}
|
{%- if attr['hx-trigger'] is defined -%}
|
||||||
{%- set child_attr = {
|
{%- set child_attr = {
|
||||||
@@ -51,6 +52,9 @@
|
|||||||
'hx-swap': attr['hx-swap']
|
'hx-swap': attr['hx-swap']
|
||||||
} -%}
|
} -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
{%- if fieldReadonly and child.vars.attr.readonly is not defined -%}
|
||||||
|
{%- set child_attr = child_attr|merge({'readonly': true}) -%}
|
||||||
|
{%- endif -%}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="border border-primary-bg p-2 align-top">
|
<td class="border border-primary-bg p-2 align-top">
|
||||||
<div>{{- child.vars.label -}}</div>
|
<div>{{- child.vars.label -}}</div>
|
||||||
|
|||||||
@@ -205,7 +205,9 @@
|
|||||||
<div{% if tooltip is not null %} {{ stimulus_controller('tooltip', { 'content': tooltip }) }}{% endif %}>
|
<div{% if tooltip is not null %} {{ stimulus_controller('tooltip', { 'content': tooltip }) }}{% endif %}>
|
||||||
{%- if attr.readonly is defined %}
|
{%- if attr.readonly is defined %}
|
||||||
<div class="cursor-not-allowed">
|
<div class="cursor-not-allowed">
|
||||||
{{ parent() }}
|
<div class="pointer-events-none">
|
||||||
|
{{ parent() }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
@@ -223,6 +225,9 @@
|
|||||||
'hx-swap': attr['hx-swap']
|
'hx-swap': attr['hx-swap']
|
||||||
}) -%}
|
}) -%}
|
||||||
{%- endif -%}
|
{%- 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 -%}
|
{%- if child.vars.attr['data-tooltip'] is defined -%}
|
||||||
{%- set child_attr = child_attr|merge({'data-tooltip': child.vars.attr['data-tooltip']}) -%}
|
{%- set child_attr = child_attr|merge({'data-tooltip': child.vars.attr['data-tooltip']}) -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -358,4 +363,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user