Skip to content

Commit 87770cf

Browse files
committed
Add a switch to make the ClosureExpressionVisitor use direct field access through reflection only
1 parent 35492fe commit 87770cf

File tree

7 files changed

+201
-36
lines changed

7 files changed

+201
-36
lines changed

src/ArrayCollection.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,11 +450,13 @@ public function slice(int $offset, int|null $length = null)
450450
/** @phpstan-return Collection<TKey, T>&Selectable<TKey,T> */
451451
public function matching(Criteria $criteria)
452452
{
453+
$accessRawFieldValues = $criteria->isRawFieldValueAccessEnabled();
454+
453455
$expr = $criteria->getWhereExpression();
454456
$filtered = $this->elements;
455457

456458
if ($expr) {
457-
$visitor = new ClosureExpressionVisitor();
459+
$visitor = new ClosureExpressionVisitor($accessRawFieldValues);
458460
$filter = $visitor->dispatch($expr);
459461
$filtered = array_filter($filtered, $filter);
460462
}
@@ -464,7 +466,7 @@ public function matching(Criteria $criteria)
464466
if ($orderings) {
465467
$next = null;
466468
foreach (array_reverse($orderings) as $field => $ordering) {
467-
$next = ClosureExpressionVisitor::sortByField($field, $ordering === Order::Descending ? -1 : 1, $next);
469+
$next = ClosureExpressionVisitor::sortByField($field, $ordering === Order::Descending ? -1 : 1, $next, $accessRawFieldValues);
468470
}
469471

470472
uasort($filtered, $next);

src/Criteria.php

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,16 @@ class Criteria
3333
private int|null $firstResult = null;
3434
private int|null $maxResults = null;
3535

36+
private bool $accessRawFieldValues = false;
37+
3638
/**
3739
* Creates an instance of the class.
3840
*
3941
* @return static
4042
*/
41-
public static function create()
43+
public static function create(bool $accessRawFieldValues = false): self
4244
{
43-
return new static();
45+
return new static(accessRawFieldValues: $accessRawFieldValues);
4446
}
4547

4648
/**
@@ -67,8 +69,18 @@ public function __construct(
6769
array|null $orderings = null,
6870
int|null $firstResult = null,
6971
int|null $maxResults = null,
72+
bool $accessRawFieldValues = false,
7073
) {
71-
$this->expression = $expression;
74+
if (!$accessRawFieldValues) {
75+
Deprecation::trigger(
76+
'doctrine/collections',
77+
'https://github.com/doctrine/collections/pull/472',
78+
'Not enabling raw field value access for the Criteria matching API in %s is deprecated. Raw field access will be the only supported method in 3.0',
79+
self::class,
80+
);
81+
}
82+
$this->accessRawFieldValues = $accessRawFieldValues;
83+
7284

7385
if ($firstResult === null && func_num_args() > 2) {
7486
Deprecation::trigger(
@@ -282,4 +294,12 @@ public function setMaxResults(int|null $maxResults)
282294

283295
return $this;
284296
}
297+
298+
/**
299+
* @internal
300+
*/
301+
public function isRawFieldValueAccessEnabled(): bool
302+
{
303+
return $this->accessRawFieldValues;
304+
}
285305
}

src/Expr/ClosureExpressionVisitor.php

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use ArrayAccess;
88
use Closure;
9+
use Doctrine\Deprecations\Deprecation;
910
use RuntimeException;
1011

1112
use function array_all;
@@ -31,6 +32,11 @@
3132
*/
3233
class ClosureExpressionVisitor extends ExpressionVisitor
3334
{
35+
public function __construct(
36+
private readonly bool $accessRawFieldValues = false,
37+
) {
38+
}
39+
3440
/**
3541
* Accesses the field of a given object. This field has to be public
3642
* directly or indirectly (through an accessor get*, is*, or a magic
@@ -40,19 +46,30 @@ class ClosureExpressionVisitor extends ExpressionVisitor
4046
*
4147
* @return mixed
4248
*/
43-
public static function getObjectFieldValue(object|array $object, string $field)
49+
public static function getObjectFieldValue(object|array $object, string $field, bool $accessRawFieldValues = false)
4450
{
4551
if (str_contains($field, '.')) {
4652
[$field, $subField] = explode('.', $field, 2);
47-
$object = self::getObjectFieldValue($object, $field);
53+
$object = self::getObjectFieldValue($object, $field, $accessRawFieldValues);
4854

49-
return self::getObjectFieldValue($object, $subField);
55+
return self::getObjectFieldValue($object, $subField, $accessRawFieldValues);
5056
}
5157

5258
if (is_array($object)) {
5359
return $object[$field];
5460
}
5561

62+
if ($accessRawFieldValues) {
63+
return self::getNearestFieldValue($object, $field);
64+
}
65+
66+
Deprecation::trigger(
67+
'doctrine/collections',
68+
'https://github.com/doctrine/collections/pull/472',
69+
'Not enabling raw field value access for %s is deprecated. Raw field access will be the only supported method in 3.0',
70+
__METHOD__,
71+
);
72+
5673
$accessors = ['get', 'is', ''];
5774

5875
foreach ($accessors as $accessor) {
@@ -96,21 +113,42 @@ public static function getObjectFieldValue(object|array $object, string $field)
96113
return $object->$field;
97114
}
98115

116+
private static function getNearestFieldValue(object $object, string $field): mixed
117+
{
118+
$reflectionClass = new \ReflectionClass($object);
119+
120+
while ($reflectionClass && !$reflectionClass->hasProperty($field)) {
121+
$reflectionClass = $reflectionClass->getParentClass();
122+
}
123+
124+
if (false === $reflectionClass) {
125+
throw new RuntimeException(sprintf('Field "%s" does not exist in class %s', $field, get_class($object)));
126+
}
127+
128+
return $reflectionClass->getProperty($field)->getValue($object);
129+
}
130+
99131
/**
100132
* Helper for sorting arrays of objects based on multiple fields + orientations.
101133
*
102134
* @return Closure
103135
*/
104-
public static function sortByField(string $name, int $orientation = 1, Closure|null $next = null)
136+
public static function sortByField(string $name, int $orientation = 1, Closure|null $next = null, bool $accessRawFieldValues = false)
105137
{
138+
Deprecation::trigger(
139+
'doctrine/collections',
140+
'https://github.com/doctrine/collections/pull/472',
141+
'Not enabling raw field value access for %s is deprecated. Raw field access will be the only supported method in 3.0',
142+
__METHOD__,
143+
);
144+
106145
if (! $next) {
107146
$next = static fn (): int => 0;
108147
}
109148

110-
return static function ($a, $b) use ($name, $next, $orientation): int {
111-
$aValue = ClosureExpressionVisitor::getObjectFieldValue($a, $name);
112-
113-
$bValue = ClosureExpressionVisitor::getObjectFieldValue($b, $name);
149+
return static function ($a, $b) use ($name, $next, $orientation, $accessRawFieldValues): int {
150+
$aValue = ClosureExpressionVisitor::getObjectFieldValue($a, $name, $accessRawFieldValues);
151+
$bValue = ClosureExpressionVisitor::getObjectFieldValue($b, $name, $accessRawFieldValues);
114152

115153
if ($aValue === $bValue) {
116154
return $next($a, $b);
@@ -129,34 +167,34 @@ public function walkComparison(Comparison $comparison)
129167
$value = $comparison->getValue()->getValue();
130168

131169
return match ($comparison->getOperator()) {
132-
Comparison::EQ => static fn ($object): bool => self::getObjectFieldValue($object, $field) === $value,
133-
Comparison::NEQ => static fn ($object): bool => self::getObjectFieldValue($object, $field) !== $value,
134-
Comparison::LT => static fn ($object): bool => self::getObjectFieldValue($object, $field) < $value,
135-
Comparison::LTE => static fn ($object): bool => self::getObjectFieldValue($object, $field) <= $value,
136-
Comparison::GT => static fn ($object): bool => self::getObjectFieldValue($object, $field) > $value,
137-
Comparison::GTE => static fn ($object): bool => self::getObjectFieldValue($object, $field) >= $value,
138-
Comparison::IN => static function ($object) use ($field, $value): bool {
139-
$fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field);
170+
Comparison::EQ => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) === $value,
171+
Comparison::NEQ => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) !== $value,
172+
Comparison::LT => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) < $value,
173+
Comparison::LTE => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) <= $value,
174+
Comparison::GT => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) > $value,
175+
Comparison::GTE => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) >= $value,
176+
Comparison::IN => function ($object) use ($field, $value): bool {
177+
$fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field, $this->accessRawFieldValues);
140178

141179
return in_array($fieldValue, $value, is_scalar($fieldValue));
142180
},
143-
Comparison::NIN => static function ($object) use ($field, $value): bool {
144-
$fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field);
181+
Comparison::NIN => function ($object) use ($field, $value): bool {
182+
$fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field, $this->accessRawFieldValues);
145183

146184
return ! in_array($fieldValue, $value, is_scalar($fieldValue));
147185
},
148-
Comparison::CONTAINS => static fn ($object): bool => str_contains((string) self::getObjectFieldValue($object, $field), (string) $value),
149-
Comparison::MEMBER_OF => static function ($object) use ($field, $value): bool {
150-
$fieldValues = ClosureExpressionVisitor::getObjectFieldValue($object, $field);
186+
Comparison::CONTAINS => fn ($object): bool => str_contains((string) self::getObjectFieldValue($object, $field, $this->accessRawFieldValues), (string) $value),
187+
Comparison::MEMBER_OF => function ($object) use ($field, $value): bool {
188+
$fieldValues = ClosureExpressionVisitor::getObjectFieldValue($object, $field, $this->accessRawFieldValues);
151189

152190
if (! is_array($fieldValues)) {
153191
$fieldValues = iterator_to_array($fieldValues);
154192
}
155193

156194
return in_array($value, $fieldValues, true);
157195
},
158-
Comparison::STARTS_WITH => static fn ($object): bool => str_starts_with((string) self::getObjectFieldValue($object, $field), (string) $value),
159-
Comparison::ENDS_WITH => static fn ($object): bool => str_ends_with((string) self::getObjectFieldValue($object, $field), (string) $value),
196+
Comparison::STARTS_WITH => fn ($object): bool => str_starts_with((string) self::getObjectFieldValue($object, $field, $this->accessRawFieldValues), (string) $value),
197+
Comparison::ENDS_WITH => fn ($object): bool => str_ends_with((string) self::getObjectFieldValue($object, $field, $this->accessRawFieldValues), (string) $value),
160198
default => throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator()),
161199
};
162200
}

0 commit comments

Comments
 (0)