Files
myep/src/Form/Service/Condition/CompositeCondition.php
T

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