wip: major refactoring
This commit is contained in:
1 parent
d719b17aec
commit
11825191cf
32 files changed
+692
-466
No files matched your search
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
* Condition that evaluates participant age against specified range criteria.
|
||||
*
|
||||
* This condition checks if a participant's age (calculated from their date of birth)
|
||||
* falls within, outside, or meets specific age criteria. It's commonly used to
|
||||
* enable/disable fields based on age restrictions for services, legal requirements,
|
||||
* or business rules.
|
||||
*
|
||||
* Age Calculation:
|
||||
* - Uses participant's dateOfBirth property to calculate current age
|
||||
* - Handles null dates gracefully (condition fails if no birth date)
|
||||
* - Age is calculated as of today's date when condition is evaluated
|
||||
*
|
||||
* Range Types:
|
||||
* - Minimum age: participant must be at least X years old
|
||||
* - Maximum age: participant must be under X years old
|
||||
* - Age range: participant must be between X and Y years old
|
||||
* - Exact age: participant must be exactly X years old
|
||||
*/
|
||||
class AgeRangeCondition implements FieldConditionInterface
|
||||
{
|
||||
/**
|
||||
* Creates a new age range condition.
|
||||
*
|
||||
* @param int|null $minAge Minimum required age (inclusive), null for no minimum
|
||||
* @param int|null $maxAge Maximum allowed age (inclusive), null for no maximum
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ?int $minAge = null,
|
||||
private readonly ?int $maxAge = null,
|
||||
) {
|
||||
if (null === $minAge && null === $maxAge) {
|
||||
throw new \InvalidArgumentException('At least one of minAge or maxAge must be specified');
|
||||
}
|
||||
|
||||
if (null !== $minAge && null !== $maxAge && $minAge > $maxAge) {
|
||||
throw new \InvalidArgumentException('Minimum age cannot be greater than maximum age');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates if the participant's age meets the specified criteria.
|
||||
*
|
||||
* Calculates the participant's current age from their date of birth and
|
||||
* checks if it falls within the configured age range. Returns false if
|
||||
* the participant has no date of birth set.
|
||||
*
|
||||
* @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 (unused for age conditions)
|
||||
*
|
||||
* @return bool True if the participant's age meets the criteria, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
|
||||
if (null === $participant || null === $participant->dateOfBirth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$age = $this->calculateAge($participant->dateOfBirth);
|
||||
|
||||
// Check minimum age requirement
|
||||
if (null !== $this->minAge && $age < $this->minAge) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check maximum age requirement
|
||||
if (null !== $this->maxAge && $age > $this->maxAge) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns field names that affect age calculation.
|
||||
*
|
||||
* The age condition depends on the participant's date of birth field.
|
||||
* When this field changes, any conditions based on age should be re-evaluated.
|
||||
*
|
||||
* @return string[] Array containing 'dateOfBirth' field name
|
||||
*/
|
||||
public function getDependentFields(): array
|
||||
{
|
||||
return ['dateOfBirth'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable description of the age criteria.
|
||||
*
|
||||
* Generates a descriptive string explaining the age requirements,
|
||||
* useful for debugging and understanding condition logic.
|
||||
*
|
||||
* @return string Description of the age range criteria
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
if (null !== $this->minAge && null !== $this->maxAge) {
|
||||
if ($this->minAge === $this->maxAge) {
|
||||
return sprintf('Participant must be exactly %d years old', $this->minAge);
|
||||
}
|
||||
|
||||
return sprintf('Participant must be between %d and %d years old', $this->minAge, $this->maxAge);
|
||||
}
|
||||
|
||||
if (null !== $this->minAge) {
|
||||
return sprintf('Participant must be at least %d years old', $this->minAge);
|
||||
}
|
||||
|
||||
return sprintf('Participant must be under %d years old', $this->maxAge + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates age in years from a date of birth.
|
||||
*
|
||||
* Uses DateTimeImmutable to ensure immutable date calculations and
|
||||
* handles the calculation accurately accounting for leap years and
|
||||
* exact birth date anniversaries.
|
||||
*
|
||||
* @param \DateTimeImmutable $dateOfBirth The participant's date of birth
|
||||
*
|
||||
* @return int The calculated age in complete years
|
||||
*/
|
||||
private function calculateAge(\DateTimeImmutable $dateOfBirth): int
|
||||
{
|
||||
$today = new \DateTimeImmutable();
|
||||
|
||||
return (int) $dateOfBirth->diff($today)->y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
* Condition that checks if the participant is the applicant.
|
||||
*
|
||||
* This is used to apply field state logic (e.g., enable/disable fields)
|
||||
* specifically for the applicant participant in the booking.
|
||||
*
|
||||
* The default logic assumes the applicant is the first participant (index 0),
|
||||
* but this can be adjusted if your domain uses a different rule or property.
|
||||
*/
|
||||
class ApplicantCondition implements FieldConditionInterface
|
||||
{
|
||||
/**
|
||||
* Evaluates if the participant is the applicant.
|
||||
*
|
||||
* @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 (unused)
|
||||
*
|
||||
* @return bool True if the participant is the applicant, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
// Default: applicant is the first participant (index 0)
|
||||
return $participantIndex === 0;
|
||||
}
|
||||
|
||||
public function getDependentFields(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Checks if the participant is the applicant (index 0)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
* Composite condition that combines multiple conditions with logical operators.
|
||||
*
|
||||
* This condition enables complex field state logic by combining multiple
|
||||
* conditions using AND, OR, and NOT operators. It supports nested composition
|
||||
* for arbitrarily complex conditional logic while maintaining readability
|
||||
* and performance.
|
||||
*
|
||||
* Logical Operations:
|
||||
* - AND: All conditions must be true
|
||||
* - OR: At least one condition must be true
|
||||
* - NOT: Inverts the result of the wrapped condition
|
||||
*
|
||||
* Common Use Cases:
|
||||
* - Field requires both age restriction AND room assignment
|
||||
* - Field is enabled if service A OR service B is selected
|
||||
* - Field is disabled unless (condition A AND NOT condition B)
|
||||
* - Complex business rules combining multiple criteria
|
||||
*
|
||||
* Performance Optimization:
|
||||
* - Short-circuit evaluation for AND/OR operations
|
||||
* - Dependency tracking aggregates all child condition dependencies
|
||||
* - Lazy evaluation prevents unnecessary condition checks
|
||||
*/
|
||||
class CompositeCondition implements FieldConditionInterface
|
||||
{
|
||||
private const OPERATOR_AND = 'AND';
|
||||
private const OPERATOR_OR = 'OR';
|
||||
private const OPERATOR_NOT = 'NOT';
|
||||
|
||||
/** @var FieldConditionInterface[] */
|
||||
private readonly array $conditions;
|
||||
|
||||
/**
|
||||
* Creates a new composite condition.
|
||||
*
|
||||
* @param string $operator The logical operator (AND, OR, NOT)
|
||||
* @param FieldConditionInterface $condition Primary condition (required for all operators)
|
||||
* @param FieldConditionInterface ...$conditions Additional conditions (for AND/OR operators)
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $operator,
|
||||
FieldConditionInterface $condition,
|
||||
FieldConditionInterface ...$conditions,
|
||||
) {
|
||||
$this->validateOperator($operator);
|
||||
$this->conditions = [$condition, ...$conditions];
|
||||
$this->validateConditionCount($operator, $this->conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the composite condition using the specified logical operator.
|
||||
*
|
||||
* Applies the logical operator to all child conditions, using short-circuit
|
||||
* evaluation for optimal performance. The evaluation stops as soon as the
|
||||
* final result can be determined.
|
||||
*
|
||||
* @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 composite condition is met, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
return match ($this->operator) {
|
||||
self::OPERATOR_AND => $this->evaluateAnd($bookingDto, $participantIndex, $formData),
|
||||
self::OPERATOR_OR => $this->evaluateOr($bookingDto, $participantIndex, $formData),
|
||||
self::OPERATOR_NOT => $this->evaluateNot($bookingDto, $participantIndex, $formData),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns field names that affect any child condition.
|
||||
*
|
||||
* Aggregates dependency declarations from all child conditions to create
|
||||
* a comprehensive list of fields that trigger re-evaluation of this
|
||||
* composite condition.
|
||||
*
|
||||
* @return string[] Array of field names that affect any child condition
|
||||
*/
|
||||
public function getDependentFields(): array
|
||||
{
|
||||
$dependentFields = [];
|
||||
|
||||
foreach ($this->conditions as $condition) {
|
||||
$dependentFields = array_merge($dependentFields, $condition->getDependentFields());
|
||||
}
|
||||
|
||||
return array_unique($dependentFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable description of the composite condition.
|
||||
*
|
||||
* Generates a descriptive string that shows the logical structure and
|
||||
* descriptions of all child conditions, useful for debugging and
|
||||
* understanding complex conditional logic.
|
||||
*
|
||||
* @return string Description of the composite condition logic
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
$conditionDescriptions = array_map(
|
||||
fn (FieldConditionInterface $condition) => $condition->getDescription(),
|
||||
$this->conditions
|
||||
);
|
||||
|
||||
return match ($this->operator) {
|
||||
self::OPERATOR_AND => sprintf('(%s)', implode(' AND ', $conditionDescriptions)),
|
||||
self::OPERATOR_OR => sprintf('(%s)', implode(' OR ', $conditionDescriptions)),
|
||||
self::OPERATOR_NOT => sprintf('NOT (%s)', $conditionDescriptions[0]),
|
||||
default => 'Invalid composite condition',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a composite condition that requires all conditions to be true.
|
||||
*
|
||||
* @param FieldConditionInterface ...$conditions The conditions that must all be true
|
||||
*/
|
||||
public static function and(FieldConditionInterface ...$conditions): self
|
||||
{
|
||||
if (empty($conditions)) {
|
||||
throw new \InvalidArgumentException('AND condition requires at least one condition');
|
||||
}
|
||||
|
||||
return new self(self::OPERATOR_AND, ...$conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a composite condition that requires at least one condition to be true.
|
||||
*
|
||||
* @param FieldConditionInterface ...$conditions The conditions where at least one must be true
|
||||
*/
|
||||
public static function or(FieldConditionInterface ...$conditions): self
|
||||
{
|
||||
if (empty($conditions)) {
|
||||
throw new \InvalidArgumentException('OR condition requires at least one condition');
|
||||
}
|
||||
|
||||
return new self(self::OPERATOR_OR, ...$conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a composite condition that inverts the result of another condition.
|
||||
*
|
||||
* @param FieldConditionInterface $condition The condition to invert
|
||||
*/
|
||||
public static function not(FieldConditionInterface $condition): self
|
||||
{
|
||||
return new self(self::OPERATOR_NOT, $condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates AND logic with short-circuit evaluation.
|
||||
*
|
||||
* Returns false as soon as any condition evaluates to false,
|
||||
* avoiding unnecessary evaluation of remaining conditions.
|
||||
*/
|
||||
private function evaluateAnd(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
foreach ($this->conditions as $condition) {
|
||||
if (!$condition->evaluate($bookingDto, $participantIndex, $formData)) {
|
||||
return false; // Short-circuit: if any condition is false, result is false
|
||||
}
|
||||
}
|
||||
|
||||
return true; // All conditions evaluated to true
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates OR logic with short-circuit evaluation.
|
||||
*
|
||||
* Returns true as soon as any condition evaluates to true,
|
||||
* avoiding unnecessary evaluation of remaining conditions.
|
||||
*/
|
||||
private function evaluateOr(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
foreach ($this->conditions as $condition) {
|
||||
if ($condition->evaluate($bookingDto, $participantIndex, $formData)) {
|
||||
return true; // Short-circuit: if any condition is true, result is true
|
||||
}
|
||||
}
|
||||
|
||||
return false; // No conditions evaluated to true
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates NOT logic by inverting the result of the single condition.
|
||||
*/
|
||||
private function evaluateNot(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
return !$this->conditions[0]->evaluate($bookingDto, $participantIndex, $formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the operator is supported.
|
||||
*/
|
||||
private function validateOperator(string $operator): void
|
||||
{
|
||||
$validOperators = [self::OPERATOR_AND, self::OPERATOR_OR, self::OPERATOR_NOT];
|
||||
|
||||
if (!in_array($operator, $validOperators, true)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid operator "%s". Valid operators: %s', $operator, implode(', ', $validOperators)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates condition count based on operator requirements.
|
||||
*/
|
||||
private function validateConditionCount(string $operator, array $conditions): void
|
||||
{
|
||||
$conditionCount = count($conditions);
|
||||
|
||||
if (self::OPERATOR_NOT === $operator && 1 !== $conditionCount) {
|
||||
throw new \InvalidArgumentException('NOT operator requires exactly one condition');
|
||||
}
|
||||
|
||||
if (in_array($operator, [self::OPERATOR_AND, self::OPERATOR_OR], true) && $conditionCount < 1) {
|
||||
throw new \InvalidArgumentException(sprintf('%s operator requires at least one condition', $operator));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
* Condition that checks if a participant's personal data is mutable in the edit flow.
|
||||
*
|
||||
* This is used to set fields as readonly or disabled if the booking or participant
|
||||
* is not allowed to be modified (e.g., after a certain workflow step or status).
|
||||
*
|
||||
* The logic assumes the BookingDtoInterface or its participant DTOs expose a
|
||||
* 'personalDataMutable' property or method. Adjust as needed for your domain.
|
||||
*/
|
||||
class MutabilityCondition implements FieldConditionInterface
|
||||
{
|
||||
/**
|
||||
* Evaluates if the participant's personal data is mutable.
|
||||
*
|
||||
* @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 (unused)
|
||||
*
|
||||
* @return bool True if the participant's data is mutable, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
if (null === $participant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $participant->mutable;
|
||||
}
|
||||
|
||||
public function getDependentFields(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Checks if the participant\'s personal data is mutable (edit flow)';
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user