235 lines
8.6 KiB
PHP
235 lines
8.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Form\Service\Condition;
|
|
|
|
use App\Form\Model\BookingDto;
|
|
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 BookingDto $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(BookingDto $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(BookingDto $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(BookingDto $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(BookingDto $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 (false === 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));
|
|
}
|
|
}
|
|
}
|