266 lines
9.8 KiB
PHP
266 lines
9.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Form\ParticipantFieldHandler\Condition;
|
|
|
|
use App\Form\Model\BookingDtoInterface;
|
|
|
|
/**
|
|
* Condition that evaluates field states based on other field values.
|
|
*
|
|
* This condition enables complex field interdependencies by checking the current
|
|
* value of one or more form fields against specified criteria. It supports various
|
|
* comparison operations and can handle both simple value checks and complex
|
|
* multi-field dependencies.
|
|
*
|
|
* Common Use Cases:
|
|
* - Enable/disable fields based on checkbox selections
|
|
* - Show/hide fields based on dropdown selections
|
|
* - Make fields readonly when certain conditions are met
|
|
* - Create wizard-like forms with conditional steps
|
|
*
|
|
* Supported Comparisons:
|
|
* - Equality: field equals specific value
|
|
* - Non-equality: field does not equal specific value
|
|
* - Inclusion: field value is in a set of allowed values
|
|
* - Exclusion: field value is not in a set of values
|
|
* - Empty/null checks: field is empty or has value
|
|
*/
|
|
class FieldValueCondition implements FieldConditionInterface
|
|
{
|
|
private const OPERATOR_EQUALS = 'equals';
|
|
private const OPERATOR_NOT_EQUALS = 'not_equals';
|
|
private const OPERATOR_IN = 'in';
|
|
private const OPERATOR_NOT_IN = 'not_in';
|
|
private const OPERATOR_EMPTY = 'empty';
|
|
private const OPERATOR_NOT_EMPTY = 'not_empty';
|
|
|
|
/**
|
|
* Creates a new field value condition.
|
|
*
|
|
* @param string $fieldName The name of the field to check
|
|
* @param string $operator The comparison operator to use
|
|
* @param mixed $expectedValue The expected value(s) for comparison
|
|
*/
|
|
public function __construct(
|
|
private readonly string $fieldName,
|
|
private readonly string $operator,
|
|
private readonly mixed $expectedValue = null,
|
|
) {
|
|
$this->validateOperator($operator);
|
|
$this->validateExpectedValue($operator, $expectedValue);
|
|
}
|
|
|
|
/**
|
|
* Evaluates the field value condition against current form data.
|
|
*
|
|
* Retrieves the current value of the specified field and compares it
|
|
* against the expected value using the configured operator. Supports
|
|
* both participant-level fields and booking-level fields.
|
|
*
|
|
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
|
* @param int $participantIndex The index of the participant being evaluated
|
|
* @param array<string, mixed> $formData Current form data for condition evaluation
|
|
*
|
|
* @return bool True if the field value meets the condition criteria, false otherwise
|
|
*/
|
|
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
|
{
|
|
$fieldValue = $this->getFieldValue($formData, $participantIndex, $bookingDto);
|
|
|
|
return match ($this->operator) {
|
|
self::OPERATOR_EQUALS => $this->compareEquals($fieldValue, $this->expectedValue),
|
|
self::OPERATOR_NOT_EQUALS => !$this->compareEquals($fieldValue, $this->expectedValue),
|
|
self::OPERATOR_IN => $this->compareIn($fieldValue, $this->expectedValue),
|
|
self::OPERATOR_NOT_IN => !$this->compareIn($fieldValue, $this->expectedValue),
|
|
self::OPERATOR_EMPTY => $this->isEmpty($fieldValue),
|
|
self::OPERATOR_NOT_EMPTY => !$this->isEmpty($fieldValue),
|
|
default => false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns the field names that this condition depends on.
|
|
*
|
|
* This condition depends on the field being evaluated, so any changes
|
|
* to that field should trigger re-evaluation of dependent field states.
|
|
*
|
|
* @return string[] Array containing the field name being evaluated
|
|
*/
|
|
public function getDependentFields(): array
|
|
{
|
|
return [$this->fieldName];
|
|
}
|
|
|
|
/**
|
|
* Returns a human-readable description of the field value condition.
|
|
*
|
|
* Generates a descriptive string explaining what field value is being
|
|
* checked and what the expected criteria are.
|
|
*
|
|
* @return string Description of the field value condition
|
|
*/
|
|
public function getDescription(): string
|
|
{
|
|
$valueDescription = is_array($this->expectedValue)
|
|
? '['.implode(', ', $this->expectedValue).']'
|
|
: (string) $this->expectedValue;
|
|
|
|
return match ($this->operator) {
|
|
self::OPERATOR_EQUALS => sprintf('Field "%s" equals %s', $this->fieldName, $valueDescription),
|
|
self::OPERATOR_NOT_EQUALS => sprintf('Field "%s" does not equal %s', $this->fieldName, $valueDescription),
|
|
self::OPERATOR_IN => sprintf('Field "%s" is in %s', $this->fieldName, $valueDescription),
|
|
self::OPERATOR_NOT_IN => sprintf('Field "%s" is not in %s', $this->fieldName, $valueDescription),
|
|
self::OPERATOR_EMPTY => sprintf('Field "%s" is empty', $this->fieldName),
|
|
self::OPERATOR_NOT_EMPTY => sprintf('Field "%s" is not empty', $this->fieldName),
|
|
default => sprintf('Field "%s" %s %s', $this->fieldName, $this->operator, $valueDescription),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a condition for field equality.
|
|
*
|
|
* @param string $fieldName The field to check
|
|
* @param mixed $expectedValue The expected value
|
|
*/
|
|
public static function equals(string $fieldName, mixed $expectedValue): self
|
|
{
|
|
return new self($fieldName, self::OPERATOR_EQUALS, $expectedValue);
|
|
}
|
|
|
|
/**
|
|
* Creates a condition for field non-equality.
|
|
*
|
|
* @param string $fieldName The field to check
|
|
* @param mixed $expectedValue The value that should not match
|
|
*/
|
|
public static function notEquals(string $fieldName, mixed $expectedValue): self
|
|
{
|
|
return new self($fieldName, self::OPERATOR_NOT_EQUALS, $expectedValue);
|
|
}
|
|
|
|
/**
|
|
* Creates a condition for field value inclusion.
|
|
*
|
|
* @param string $fieldName The field to check
|
|
* @param array<string, mixed> $allowedValues The allowed values
|
|
*/
|
|
public static function in(string $fieldName, array $allowedValues): self
|
|
{
|
|
return new self($fieldName, self::OPERATOR_IN, $allowedValues);
|
|
}
|
|
|
|
/**
|
|
* Creates a condition for field emptiness.
|
|
*
|
|
* @param string $fieldName The field to check
|
|
*/
|
|
public static function empty(string $fieldName): self
|
|
{
|
|
return new self($fieldName, self::OPERATOR_EMPTY);
|
|
}
|
|
|
|
/**
|
|
* Creates a condition for field non-emptiness.
|
|
*
|
|
* @param string $fieldName The field to check
|
|
*/
|
|
public static function isNotEmpty(string $fieldName): self
|
|
{
|
|
return new self($fieldName, self::OPERATOR_NOT_EMPTY);
|
|
}
|
|
|
|
/**
|
|
* Retrieves field value from form data or participant data.
|
|
*/
|
|
private function getFieldValue(array $formData, int $participantIndex, BookingDtoInterface $bookingDto): mixed
|
|
{
|
|
// First check participant-specific form data
|
|
if (isset($formData['participants'][$participantIndex][$this->fieldName])) {
|
|
return $formData['participants'][$participantIndex][$this->fieldName];
|
|
}
|
|
|
|
// Then check participant DTO data
|
|
$participant = $bookingDto->getParticipant($participantIndex);
|
|
if (null !== $participant && property_exists($participant, $this->fieldName)) {
|
|
return $participant->{$this->fieldName};
|
|
}
|
|
|
|
// Finally check booking-level form data
|
|
return $formData[$this->fieldName] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Compares two values for equality, handling type coercion.
|
|
*/
|
|
private function compareEquals(mixed $fieldValue, mixed $expectedValue): bool
|
|
{
|
|
// Handle string/number coercion common in form data
|
|
if (is_string($fieldValue) && is_numeric($expectedValue)) {
|
|
return $fieldValue === (string) $expectedValue;
|
|
}
|
|
|
|
if (is_numeric($fieldValue) && is_string($expectedValue)) {
|
|
return (string) $fieldValue === $expectedValue;
|
|
}
|
|
|
|
return $fieldValue === $expectedValue;
|
|
}
|
|
|
|
/**
|
|
* Checks if field value is in array of expected values.
|
|
*/
|
|
private function compareIn(mixed $fieldValue, array $expectedValues): bool
|
|
{
|
|
foreach ($expectedValues as $expectedValue) {
|
|
if ($this->compareEquals($fieldValue, $expectedValue)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if a value is considered empty.
|
|
*/
|
|
private function isEmpty(mixed $value): bool
|
|
{
|
|
return null === $value || '' === $value || (is_array($value) && empty($value));
|
|
}
|
|
|
|
/**
|
|
* Validates that the operator is supported.
|
|
*/
|
|
private function validateOperator(string $operator): void
|
|
{
|
|
$validOperators = [
|
|
self::OPERATOR_EQUALS,
|
|
self::OPERATOR_NOT_EQUALS,
|
|
self::OPERATOR_IN,
|
|
self::OPERATOR_NOT_IN,
|
|
self::OPERATOR_EMPTY,
|
|
self::OPERATOR_NOT_EMPTY,
|
|
];
|
|
|
|
if (!in_array($operator, $validOperators, true)) {
|
|
throw new \InvalidArgumentException(sprintf('Invalid operator "%s". Valid operators: %s', $operator, implode(', ', $validOperators)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates expected value based on operator requirements.
|
|
*/
|
|
private function validateExpectedValue(string $operator, mixed $expectedValue): void
|
|
{
|
|
if (in_array($operator, [self::OPERATOR_IN, self::OPERATOR_NOT_IN], true) && !is_array($expectedValue)) {
|
|
throw new \InvalidArgumentException(sprintf('Operator "%s" requires expectedValue to be an array', $operator));
|
|
}
|
|
|
|
if (in_array($operator, [self::OPERATOR_EMPTY, self::OPERATOR_NOT_EMPTY], true) && null !== $expectedValue) {
|
|
throw new \InvalidArgumentException(sprintf('Operator "%s" does not accept expectedValue parameter', $operator));
|
|
}
|
|
}
|
|
}
|