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 $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)); } } }