summaryrefslogtreecommitdiff
path: root/vendor/doctrine/collections/src/Expr/ClosureExpressionVisitor.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/collections/src/Expr/ClosureExpressionVisitor.php')
-rw-r--r--vendor/doctrine/collections/src/Expr/ClosureExpressionVisitor.php222
1 files changed, 222 insertions, 0 deletions
diff --git a/vendor/doctrine/collections/src/Expr/ClosureExpressionVisitor.php b/vendor/doctrine/collections/src/Expr/ClosureExpressionVisitor.php
new file mode 100644
index 0000000..4319b9b
--- /dev/null
+++ b/vendor/doctrine/collections/src/Expr/ClosureExpressionVisitor.php
@@ -0,0 +1,222 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\Common\Collections\Expr;
6
7use ArrayAccess;
8use Closure;
9use RuntimeException;
10
11use function explode;
12use function in_array;
13use function is_array;
14use function is_scalar;
15use function iterator_to_array;
16use function method_exists;
17use function preg_match;
18use function preg_replace_callback;
19use function str_contains;
20use function str_ends_with;
21use function str_starts_with;
22use function strtoupper;
23
24/**
25 * Walks an expression graph and turns it into a PHP closure.
26 *
27 * This closure can be used with {@Collection#filter()} and is used internally
28 * by {@ArrayCollection#select()}.
29 */
30class ClosureExpressionVisitor extends ExpressionVisitor
31{
32 /**
33 * Accesses the field of a given object. This field has to be public
34 * directly or indirectly (through an accessor get*, is*, or a magic
35 * method, __get, __call).
36 *
37 * @param object|mixed[] $object
38 *
39 * @return mixed
40 */
41 public static function getObjectFieldValue(object|array $object, string $field)
42 {
43 if (str_contains($field, '.')) {
44 [$field, $subField] = explode('.', $field, 2);
45 $object = self::getObjectFieldValue($object, $field);
46
47 return self::getObjectFieldValue($object, $subField);
48 }
49
50 if (is_array($object)) {
51 return $object[$field];
52 }
53
54 $accessors = ['get', 'is', ''];
55
56 foreach ($accessors as $accessor) {
57 $accessor .= $field;
58
59 if (method_exists($object, $accessor)) {
60 return $object->$accessor();
61 }
62 }
63
64 if (preg_match('/^is[A-Z]+/', $field) === 1 && method_exists($object, $field)) {
65 return $object->$field();
66 }
67
68 // __call should be triggered for get.
69 $accessor = $accessors[0] . $field;
70
71 if (method_exists($object, '__call')) {
72 return $object->$accessor();
73 }
74
75 if ($object instanceof ArrayAccess) {
76 return $object[$field];
77 }
78
79 if (isset($object->$field)) {
80 return $object->$field;
81 }
82
83 // camelcase field name to support different variable naming conventions
84 $ccField = preg_replace_callback('/_(.?)/', static fn ($matches) => strtoupper((string) $matches[1]), $field);
85
86 foreach ($accessors as $accessor) {
87 $accessor .= $ccField;
88
89 if (method_exists($object, $accessor)) {
90 return $object->$accessor();
91 }
92 }
93
94 return $object->$field;
95 }
96
97 /**
98 * Helper for sorting arrays of objects based on multiple fields + orientations.
99 *
100 * @return Closure
101 */
102 public static function sortByField(string $name, int $orientation = 1, Closure|null $next = null)
103 {
104 if (! $next) {
105 $next = static fn (): int => 0;
106 }
107
108 return static function ($a, $b) use ($name, $next, $orientation): int {
109 $aValue = ClosureExpressionVisitor::getObjectFieldValue($a, $name);
110
111 $bValue = ClosureExpressionVisitor::getObjectFieldValue($b, $name);
112
113 if ($aValue === $bValue) {
114 return $next($a, $b);
115 }
116
117 return ($aValue > $bValue ? 1 : -1) * $orientation;
118 };
119 }
120
121 /**
122 * {@inheritDoc}
123 */
124 public function walkComparison(Comparison $comparison)
125 {
126 $field = $comparison->getField();
127 $value = $comparison->getValue()->getValue();
128
129 return match ($comparison->getOperator()) {
130 Comparison::EQ => static fn ($object): bool => self::getObjectFieldValue($object, $field) === $value,
131 Comparison::NEQ => static fn ($object): bool => self::getObjectFieldValue($object, $field) !== $value,
132 Comparison::LT => static fn ($object): bool => self::getObjectFieldValue($object, $field) < $value,
133 Comparison::LTE => static fn ($object): bool => self::getObjectFieldValue($object, $field) <= $value,
134 Comparison::GT => static fn ($object): bool => self::getObjectFieldValue($object, $field) > $value,
135 Comparison::GTE => static fn ($object): bool => self::getObjectFieldValue($object, $field) >= $value,
136 Comparison::IN => static function ($object) use ($field, $value): bool {
137 $fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field);
138
139 return in_array($fieldValue, $value, is_scalar($fieldValue));
140 },
141 Comparison::NIN => static function ($object) use ($field, $value): bool {
142 $fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field);
143
144 return ! in_array($fieldValue, $value, is_scalar($fieldValue));
145 },
146 Comparison::CONTAINS => static fn ($object): bool => str_contains((string) self::getObjectFieldValue($object, $field), (string) $value),
147 Comparison::MEMBER_OF => static function ($object) use ($field, $value): bool {
148 $fieldValues = ClosureExpressionVisitor::getObjectFieldValue($object, $field);
149
150 if (! is_array($fieldValues)) {
151 $fieldValues = iterator_to_array($fieldValues);
152 }
153
154 return in_array($value, $fieldValues, true);
155 },
156 Comparison::STARTS_WITH => static fn ($object): bool => str_starts_with((string) self::getObjectFieldValue($object, $field), (string) $value),
157 Comparison::ENDS_WITH => static fn ($object): bool => str_ends_with((string) self::getObjectFieldValue($object, $field), (string) $value),
158 default => throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator()),
159 };
160 }
161
162 /**
163 * {@inheritDoc}
164 */
165 public function walkValue(Value $value)
166 {
167 return $value->getValue();
168 }
169
170 /**
171 * {@inheritDoc}
172 */
173 public function walkCompositeExpression(CompositeExpression $expr)
174 {
175 $expressionList = [];
176
177 foreach ($expr->getExpressionList() as $child) {
178 $expressionList[] = $this->dispatch($child);
179 }
180
181 return match ($expr->getType()) {
182 CompositeExpression::TYPE_AND => $this->andExpressions($expressionList),
183 CompositeExpression::TYPE_OR => $this->orExpressions($expressionList),
184 CompositeExpression::TYPE_NOT => $this->notExpression($expressionList),
185 default => throw new RuntimeException('Unknown composite ' . $expr->getType()),
186 };
187 }
188
189 /** @param callable[] $expressions */
190 private function andExpressions(array $expressions): Closure
191 {
192 return static function ($object) use ($expressions): bool {
193 foreach ($expressions as $expression) {
194 if (! $expression($object)) {
195 return false;
196 }
197 }
198
199 return true;
200 };
201 }
202
203 /** @param callable[] $expressions */
204 private function orExpressions(array $expressions): Closure
205 {
206 return static function ($object) use ($expressions): bool {
207 foreach ($expressions as $expression) {
208 if ($expression($object)) {
209 return true;
210 }
211 }
212
213 return false;
214 };
215 }
216
217 /** @param callable[] $expressions */
218 private function notExpression(array $expressions): Closure
219 {
220 return static fn ($object) => ! $expressions[0]($object);
221 }
222}