1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Data\Model;
6:
7: use Atk4\Core\ContainerTrait;
8: use Atk4\Data\Exception;
9: use Atk4\Data\Field;
10: use Atk4\Data\Model;
11: use Atk4\Data\Model\Scope\AbstractScope;
12: use Atk4\Data\Persistence\Sql\Expressionable;
13:
14: /**
15: * @property array<AbstractScope> $elements
16: */
17: class Scope extends AbstractScope
18: {
19: use ContainerTrait;
20:
21: // junction definitions
22: public const OR = 'OR';
23: public const AND = 'AND';
24:
25: /** @var self::AND|self::OR Junction to use in case more than one element. */
26: protected $junction = self::AND;
27:
28: /**
29: * Create a Scope from array of condition objects or condition arrays.
30: *
31: * @param array<int, AbstractScope|Expressionable|array{string|Expressionable, 1?: mixed, 2?: mixed}> $conditions
32: */
33: public function __construct(array $conditions = [], string $junction = self::AND)
34: {
35: if (!in_array($junction, [self::OR, self::AND], true)) {
36: throw (new Exception('Using invalid CompondCondition junction'))
37: ->addMoreInfo('junction', $junction);
38: }
39:
40: $this->junction = $junction;
41:
42: foreach ($conditions as $condition) {
43: if (!$condition instanceof AbstractScope) {
44: if ($condition instanceof Expressionable && !$condition instanceof Field) {
45: $condition = [$condition];
46: }
47: $condition = new Scope\Condition(...$condition);
48: }
49:
50: $this->add($condition);
51: }
52: }
53:
54: public function __clone()
55: {
56: foreach ($this->elements as $k => $nestedCondition) {
57: $this->elements[$k] = clone $nestedCondition;
58: if ($this->elements[$k]->issetOwner()) {
59: $this->elements[$k]->unsetOwner();
60: }
61: $this->elements[$k]->setOwner($this);
62: $this->elements[$k]->shortName = $nestedCondition->shortName;
63: }
64: if ($this->issetOwner()) {
65: $this->unsetOwner();
66: }
67: $this->shortName = null; // @phpstan-ignore-line
68: }
69:
70: /**
71: * @param AbstractScope|array<int, AbstractScope|Expressionable|array{string|Expressionable, 1?: mixed, 2?: mixed}>|string|Expressionable $field
72: * @param ($field is string|Expressionable ? ($value is null ? mixed : string) : never) $operator
73: * @param ($operator is string ? mixed : never) $value
74: *
75: * @return $this
76: */
77: public function addCondition($field, $operator = null, $value = null)
78: {
79: if ('func_num_args'() === 1 && $field instanceof AbstractScope) {
80: $condition = $field;
81: } elseif ('func_num_args'() === 1 && is_array($field)) {
82: $condition = static::createAnd(...$field);
83: } else {
84: $condition = new Scope\Condition(...'func_get_args'());
85: }
86:
87: $this->add($condition);
88:
89: return $this;
90: }
91:
92: /**
93: * Return array of nested conditions.
94: *
95: * @return array<AbstractScope>
96: */
97: public function getNestedConditions()
98: {
99: return $this->elements;
100: }
101:
102: #[\Override]
103: protected function onChangeModel(): void
104: {
105: foreach ($this->elements as $nestedCondition) {
106: $nestedCondition->onChangeModel();
107: }
108: }
109:
110: #[\Override]
111: public function isEmpty(): bool
112: {
113: return count($this->elements) === 0;
114: }
115:
116: #[\Override]
117: public function isCompound(): bool
118: {
119: return count($this->elements) > 1;
120: }
121:
122: /**
123: * @return self::AND|self::OR
124: */
125: public function getJunction(): string
126: {
127: return $this->junction;
128: }
129:
130: /**
131: * Checks if junction is OR.
132: */
133: public function isOr(): bool
134: {
135: return $this->junction === self::OR;
136: }
137:
138: /**
139: * Checks if junction is AND.
140: */
141: public function isAnd(): bool
142: {
143: return $this->junction === self::AND;
144: }
145:
146: #[\Override]
147: public function clear(): self
148: {
149: $this->elements = [];
150:
151: return $this;
152: }
153:
154: #[\Override]
155: public function simplify(): AbstractScope
156: {
157: if (count($this->elements) !== 1) {
158: return $this;
159: }
160:
161: $component = reset($this->elements);
162:
163: return $component->simplify();
164: }
165:
166: /**
167: * Use De Morgan's laws to negate.
168: */
169: #[\Override]
170: public function negate(): self
171: {
172: $this->junction = $this->junction === self::OR ? self::AND : self::OR;
173:
174: foreach ($this->elements as $nestedCondition) {
175: $nestedCondition->negate();
176: }
177:
178: return $this;
179: }
180:
181: #[\Override]
182: public function toWords(Model $model = null): string
183: {
184: $parts = [];
185: foreach ($this->elements as $nestedCondition) {
186: $words = $nestedCondition->toWords($model);
187:
188: $parts[] = $this->isCompound() && $nestedCondition->isCompound() ? '(' . $words . ')' : $words;
189: }
190:
191: $glue = ' ' . strtolower($this->junction) . ' ';
192:
193: return implode($glue, $parts);
194: }
195:
196: /**
197: * @param AbstractScope|Expressionable|array{string|Expressionable, 1?: mixed, 2?: mixed} ...$conditions
198: *
199: * @return static
200: */
201: public static function createAnd(...$conditions)
202: {
203: return new static($conditions, self::AND);
204: }
205:
206: /**
207: * @param AbstractScope|Expressionable|array{string|Expressionable, 1?: mixed, 2?: mixed} ...$conditions
208: *
209: * @return static
210: */
211: public static function createOr(...$conditions)
212: {
213: return new static($conditions, self::OR);
214: }
215: }
216: