Upstream doctrine/collections state of the touched files before 5817703073
This commit is contained in:
commit
dd6d913ae6
2 files changed
+866
No files matched your search
@@ -0,0 +1,272 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Common\Collections\Expr;
|
||||||
|
|
||||||
|
use ArrayAccess;
|
||||||
|
use Closure;
|
||||||
|
use Doctrine\Deprecations\Deprecation;
|
||||||
|
use ReflectionClass;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
use function array_all;
|
||||||
|
use function array_any;
|
||||||
|
use function explode;
|
||||||
|
use function func_get_arg;
|
||||||
|
use function func_num_args;
|
||||||
|
use function in_array;
|
||||||
|
use function is_array;
|
||||||
|
use function is_scalar;
|
||||||
|
use function iterator_to_array;
|
||||||
|
use function method_exists;
|
||||||
|
use function preg_match;
|
||||||
|
use function preg_replace_callback;
|
||||||
|
use function sprintf;
|
||||||
|
use function str_contains;
|
||||||
|
use function str_ends_with;
|
||||||
|
use function str_starts_with;
|
||||||
|
use function strtoupper;
|
||||||
|
|
||||||
|
use const PHP_VERSION_ID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walks an expression graph and turns it into a PHP closure.
|
||||||
|
*
|
||||||
|
* This closure can be used with {@Collection#filter()} and is used internally
|
||||||
|
* by {@ArrayCollection#select()}.
|
||||||
|
*
|
||||||
|
* @final since 2.5
|
||||||
|
*/
|
||||||
|
class ClosureExpressionVisitor extends ExpressionVisitor
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly bool $accessRawFieldValues = false,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accesses the field of a given object. This field has to be public
|
||||||
|
* directly or indirectly (through an accessor get*, is*, or a magic
|
||||||
|
* method, __get, __call).
|
||||||
|
*
|
||||||
|
* @param object|mixed[] $object
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function getObjectFieldValue(object|array $object, string $field, /* bool $accessRawFieldValues = false */)
|
||||||
|
{
|
||||||
|
$accessRawFieldValues = 3 <= func_num_args() ? func_get_arg(2) : false;
|
||||||
|
|
||||||
|
if (str_contains($field, '.')) {
|
||||||
|
[$field, $subField] = explode('.', $field, 2);
|
||||||
|
$object = self::getObjectFieldValue($object, $field, $accessRawFieldValues);
|
||||||
|
|
||||||
|
return self::getObjectFieldValue($object, $subField, $accessRawFieldValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($object)) {
|
||||||
|
return $object[$field];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($accessRawFieldValues) {
|
||||||
|
return self::getNearestFieldValue($object, $field);
|
||||||
|
}
|
||||||
|
|
||||||
|
Deprecation::trigger(
|
||||||
|
'doctrine/collections',
|
||||||
|
'https://github.com/doctrine/collections/pull/472',
|
||||||
|
'Not enabling raw field value access for %s is deprecated. Raw field access will be the only supported method in 3.0',
|
||||||
|
__METHOD__,
|
||||||
|
);
|
||||||
|
|
||||||
|
$accessors = ['get', 'is', ''];
|
||||||
|
|
||||||
|
foreach ($accessors as $accessor) {
|
||||||
|
$accessor .= $field;
|
||||||
|
|
||||||
|
if (method_exists($object, $accessor)) {
|
||||||
|
return $object->$accessor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^is[A-Z]+/', $field) === 1 && method_exists($object, $field)) {
|
||||||
|
return $object->$field();
|
||||||
|
}
|
||||||
|
|
||||||
|
// __call should be triggered for get.
|
||||||
|
$accessor = $accessors[0] . $field;
|
||||||
|
|
||||||
|
if (method_exists($object, '__call')) {
|
||||||
|
return $object->$accessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($object instanceof ArrayAccess) {
|
||||||
|
return $object[$field];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($object->$field)) {
|
||||||
|
return $object->$field;
|
||||||
|
}
|
||||||
|
|
||||||
|
// camelcase field name to support different variable naming conventions
|
||||||
|
$ccField = preg_replace_callback('/_(.?)/', static fn ($matches) => strtoupper((string) $matches[1]), $field);
|
||||||
|
|
||||||
|
foreach ($accessors as $accessor) {
|
||||||
|
$accessor .= $ccField;
|
||||||
|
|
||||||
|
if (method_exists($object, $accessor)) {
|
||||||
|
return $object->$accessor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $object->$field;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getNearestFieldValue(object $object, string $field): mixed
|
||||||
|
{
|
||||||
|
$reflectionClass = new ReflectionClass($object);
|
||||||
|
|
||||||
|
while ($reflectionClass && ! $reflectionClass->hasProperty($field)) {
|
||||||
|
$reflectionClass = $reflectionClass->getParentClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($reflectionClass === false) {
|
||||||
|
throw new RuntimeException(sprintf('Field "%s" does not exist in class "%s"', $field, $object::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
$property = $reflectionClass->getProperty($field);
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID >= 80400) {
|
||||||
|
return $property->getRawValue($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $property->getValue($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for sorting arrays of objects based on multiple fields + orientations.
|
||||||
|
*
|
||||||
|
* @return Closure
|
||||||
|
*/
|
||||||
|
public static function sortByField(string $name, int $orientation = 1, Closure|null $next = null, /* bool $accessRawFieldValues = false */)
|
||||||
|
{
|
||||||
|
$accessRawFieldValues = 4 <= func_num_args() ? func_get_arg(3) : false;
|
||||||
|
|
||||||
|
if (! $accessRawFieldValues) {
|
||||||
|
Deprecation::trigger(
|
||||||
|
'doctrine/collections',
|
||||||
|
'https://github.com/doctrine/collections/pull/472',
|
||||||
|
'Not enabling raw field value access for %s is deprecated. Raw field access will be the only supported method in 3.0',
|
||||||
|
__METHOD__,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $next) {
|
||||||
|
$next = static fn (): int => 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static function ($a, $b) use ($name, $next, $orientation, $accessRawFieldValues): int {
|
||||||
|
$aValue = ClosureExpressionVisitor::getObjectFieldValue($a, $name, $accessRawFieldValues);
|
||||||
|
$bValue = ClosureExpressionVisitor::getObjectFieldValue($b, $name, $accessRawFieldValues);
|
||||||
|
|
||||||
|
if ($aValue === $bValue) {
|
||||||
|
return $next($a, $b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($aValue > $bValue ? 1 : -1) * $orientation;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function walkComparison(Comparison $comparison)
|
||||||
|
{
|
||||||
|
$field = $comparison->getField();
|
||||||
|
$value = $comparison->getValue()->getValue();
|
||||||
|
|
||||||
|
return match ($comparison->getOperator()) {
|
||||||
|
Comparison::EQ => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) === $value,
|
||||||
|
Comparison::NEQ => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) !== $value,
|
||||||
|
Comparison::LT => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) < $value,
|
||||||
|
Comparison::LTE => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) <= $value,
|
||||||
|
Comparison::GT => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) > $value,
|
||||||
|
Comparison::GTE => fn ($object): bool => self::getObjectFieldValue($object, $field, $this->accessRawFieldValues) >= $value,
|
||||||
|
Comparison::IN => function ($object) use ($field, $value): bool {
|
||||||
|
$fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field, $this->accessRawFieldValues);
|
||||||
|
|
||||||
|
return in_array($fieldValue, $value, is_scalar($fieldValue));
|
||||||
|
},
|
||||||
|
Comparison::NIN => function ($object) use ($field, $value): bool {
|
||||||
|
$fieldValue = ClosureExpressionVisitor::getObjectFieldValue($object, $field, $this->accessRawFieldValues);
|
||||||
|
|
||||||
|
return ! in_array($fieldValue, $value, is_scalar($fieldValue));
|
||||||
|
},
|
||||||
|
Comparison::CONTAINS => fn ($object): bool => str_contains((string) self::getObjectFieldValue($object, $field, $this->accessRawFieldValues), (string) $value),
|
||||||
|
Comparison::MEMBER_OF => function ($object) use ($field, $value): bool {
|
||||||
|
$fieldValues = ClosureExpressionVisitor::getObjectFieldValue($object, $field, $this->accessRawFieldValues);
|
||||||
|
|
||||||
|
if (! is_array($fieldValues)) {
|
||||||
|
$fieldValues = iterator_to_array($fieldValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($value, $fieldValues, true);
|
||||||
|
},
|
||||||
|
Comparison::STARTS_WITH => fn ($object): bool => str_starts_with((string) self::getObjectFieldValue($object, $field, $this->accessRawFieldValues), (string) $value),
|
||||||
|
Comparison::ENDS_WITH => fn ($object): bool => str_ends_with((string) self::getObjectFieldValue($object, $field, $this->accessRawFieldValues), (string) $value),
|
||||||
|
default => throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function walkValue(Value $value)
|
||||||
|
{
|
||||||
|
return $value->getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function walkCompositeExpression(CompositeExpression $expr)
|
||||||
|
{
|
||||||
|
$expressionList = [];
|
||||||
|
|
||||||
|
foreach ($expr->getExpressionList() as $child) {
|
||||||
|
$expressionList[] = $this->dispatch($child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($expr->getType()) {
|
||||||
|
CompositeExpression::TYPE_AND => $this->andExpressions($expressionList),
|
||||||
|
CompositeExpression::TYPE_OR => $this->orExpressions($expressionList),
|
||||||
|
CompositeExpression::TYPE_NOT => $this->notExpression($expressionList),
|
||||||
|
default => throw new RuntimeException('Unknown composite ' . $expr->getType()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param callable[] $expressions */
|
||||||
|
private function andExpressions(array $expressions): Closure
|
||||||
|
{
|
||||||
|
return static fn ($object): bool => array_all(
|
||||||
|
$expressions,
|
||||||
|
static fn (callable $expression): bool => (bool) $expression($object),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param callable[] $expressions */
|
||||||
|
private function orExpressions(array $expressions): Closure
|
||||||
|
{
|
||||||
|
return static fn ($object): bool => array_any(
|
||||||
|
$expressions,
|
||||||
|
static fn (callable $expression): bool => (bool) $expression($object),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param callable[] $expressions */
|
||||||
|
private function notExpression(array $expressions): Closure
|
||||||
|
{
|
||||||
|
return static fn ($object) => ! $expressions[0]($object);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,594 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Doctrine\Tests\Common\Collections;
|
||||||
|
|
||||||
|
use ArrayAccess;
|
||||||
|
use ArrayIterator;
|
||||||
|
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
|
||||||
|
use Doctrine\Common\Collections\Expr\Comparison;
|
||||||
|
use Doctrine\Common\Collections\Expr\CompositeExpression;
|
||||||
|
use Doctrine\Common\Collections\ExpressionBuilder;
|
||||||
|
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||||
|
use PHPUnit\Framework\Attributes\Group;
|
||||||
|
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||||
|
use PHPUnit\Framework\Attributes\RequiresPhp;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use RuntimeException;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
use function usort;
|
||||||
|
|
||||||
|
#[Group('DDC-1637')]
|
||||||
|
class ClosureExpressionVisitorTest extends TestCase
|
||||||
|
{
|
||||||
|
use VerifyDeprecations;
|
||||||
|
|
||||||
|
private ClosureExpressionVisitor $visitor;
|
||||||
|
|
||||||
|
private ExpressionBuilder $builder;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->visitor = new ClosureExpressionVisitor(true);
|
||||||
|
$this->builder = new ExpressionBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmbeddedObjectComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->eq('foo.foo', 1));
|
||||||
|
$this->assertTrue($closure(new TestObject(new TestObject(1))));
|
||||||
|
$this->assertFalse($closure(new TestObject(new TestObject(2))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetEmbeddedObjectFieldValueAccessingRawValue(): void
|
||||||
|
{
|
||||||
|
$object = new TestObject(new TestObjectPrivatePropertyOnly(42));
|
||||||
|
|
||||||
|
self::assertSame(42, $this->visitor->getObjectFieldValue($object, 'foo.fooBar', true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[RequiresPhp('>= 8.4.0')]
|
||||||
|
public function testGetObjectFieldValueAccessingRawValueBypassingPropertyHook(): void
|
||||||
|
{
|
||||||
|
$object = new TestObjectPropertyHook();
|
||||||
|
$object->fooBar = 42;
|
||||||
|
|
||||||
|
self::assertSame(42, $this->visitor->getObjectFieldValue($object, 'fooBar', true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetObjectFieldValueAccessingRawValue(): void
|
||||||
|
{
|
||||||
|
$object = new TestObjectPrivatePropertyOnly(42);
|
||||||
|
|
||||||
|
self::assertSame(42, $this->visitor->getObjectFieldValue($object, 'fooBar', true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetObjectFieldValueFindingParentClassAccessingRawValue(): void
|
||||||
|
{
|
||||||
|
$object = new TestObjectWithPrivatePropertyInParentClass(42);
|
||||||
|
|
||||||
|
self::assertSame(42, $this->visitor->getObjectFieldValue($object, 'fooBar', true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetObjectFieldValueNonexistentFieldAccessingRawValue(): void
|
||||||
|
{
|
||||||
|
$object = new stdClass();
|
||||||
|
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
|
||||||
|
$this->visitor->getObjectFieldValue($object, 'fooBar', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[IgnoreDeprecations]
|
||||||
|
public function testGetObjectFieldValueIsAccessor(): void
|
||||||
|
{
|
||||||
|
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/collections/pull/472');
|
||||||
|
|
||||||
|
$object = new TestObject(1, 2, true);
|
||||||
|
|
||||||
|
self::assertTrue($this->visitor->getObjectFieldValue($object, 'baz'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[IgnoreDeprecations]
|
||||||
|
public function testGetObjectFieldValueIsAccessorWithIsPrefix(): void
|
||||||
|
{
|
||||||
|
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/collections/pull/472');
|
||||||
|
|
||||||
|
$object = new TestObject(1, 2, true);
|
||||||
|
|
||||||
|
self::assertTrue($this->visitor->getObjectFieldValue($object, 'isBaz'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[IgnoreDeprecations]
|
||||||
|
public function testGetObjectFieldValueIsAccessorCamelCase(): void
|
||||||
|
{
|
||||||
|
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/collections/pull/472');
|
||||||
|
|
||||||
|
$object = new TestObjectNotCamelCase(1);
|
||||||
|
|
||||||
|
self::assertEquals(1, $this->visitor->getObjectFieldValue($object, 'foo_bar'));
|
||||||
|
self::assertEquals(1, $this->visitor->getObjectFieldValue($object, 'foobar'));
|
||||||
|
self::assertEquals(1, $this->visitor->getObjectFieldValue($object, 'fooBar'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[IgnoreDeprecations]
|
||||||
|
public function testGetObjectFieldValueIsAccessorBoth(): void
|
||||||
|
{
|
||||||
|
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/collections/pull/472');
|
||||||
|
|
||||||
|
$object = new TestObjectBothCamelCaseAndUnderscore(1, 2);
|
||||||
|
|
||||||
|
self::assertEquals(2, $this->visitor->getObjectFieldValue($object, 'foo_bar'));
|
||||||
|
self::assertEquals(2, $this->visitor->getObjectFieldValue($object, 'foobar'));
|
||||||
|
self::assertEquals(2, $this->visitor->getObjectFieldValue($object, 'fooBar'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[IgnoreDeprecations]
|
||||||
|
public function testGetObjectFieldValueIsAccessorOnePublic(): void
|
||||||
|
{
|
||||||
|
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/collections/pull/472');
|
||||||
|
|
||||||
|
$object = new TestObjectPublicCamelCaseAndPrivateUnderscore(1, 2);
|
||||||
|
|
||||||
|
self::assertEquals(2, $this->visitor->getObjectFieldValue($object, 'foo_bar'));
|
||||||
|
self::assertEquals(2, $this->visitor->getObjectFieldValue($object, 'foobar'));
|
||||||
|
self::assertEquals(2, $this->visitor->getObjectFieldValue($object, 'fooBar'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[IgnoreDeprecations]
|
||||||
|
public function testGetObjectFieldValueIsAccessorBothPublic(): void
|
||||||
|
{
|
||||||
|
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/collections/pull/472');
|
||||||
|
|
||||||
|
$object = new TestObjectPublicCamelCaseAndPrivateUnderscore(1, 2);
|
||||||
|
|
||||||
|
self::assertEquals(2, $this->visitor->getObjectFieldValue($object, 'foo_bar'));
|
||||||
|
self::assertEquals(2, $this->visitor->getObjectFieldValue($object, 'foobar'));
|
||||||
|
self::assertEquals(2, $this->visitor->getObjectFieldValue($object, 'fooBar'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[IgnoreDeprecations]
|
||||||
|
public function testGetObjectFieldValueBlankAccessor(): void
|
||||||
|
{
|
||||||
|
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/collections/pull/472');
|
||||||
|
|
||||||
|
$object = new TestObjectBlankGetter(1);
|
||||||
|
|
||||||
|
self::assertEquals(1, $this->visitor->getObjectFieldValue($object, 'foobar'));
|
||||||
|
self::assertEquals(1, $this->visitor->getObjectFieldValue($object, 'fooBar'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[IgnoreDeprecations]
|
||||||
|
public function testGetObjectFieldValueMagicCallMethod(): void
|
||||||
|
{
|
||||||
|
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/collections/pull/472');
|
||||||
|
|
||||||
|
$object = new TestObject(1, 2, true, 3);
|
||||||
|
|
||||||
|
self::assertEquals(3, $this->visitor->getObjectFieldValue($object, 'qux'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[IgnoreDeprecations]
|
||||||
|
public function testGetObjectFieldValueArrayAccess(): void
|
||||||
|
{
|
||||||
|
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/collections/pull/472');
|
||||||
|
|
||||||
|
$object = self::createMock(ArrayAccess::class);
|
||||||
|
$object->expects(self::once())
|
||||||
|
->method('offsetGet')
|
||||||
|
->with('foo')
|
||||||
|
->willReturn(33);
|
||||||
|
|
||||||
|
self::assertSame(33, $this->visitor->getObjectFieldValue($object, 'foo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[IgnoreDeprecations]
|
||||||
|
public function testGetObjectFieldValuePublicPropertyIsNull(): void
|
||||||
|
{
|
||||||
|
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/collections/pull/472');
|
||||||
|
|
||||||
|
$object = new stdClass();
|
||||||
|
$object->foo = null;
|
||||||
|
|
||||||
|
self::assertSame(null, $this->visitor->getObjectFieldValue($object, 'foo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkEqualsComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->eq('foo', 1));
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject(1)));
|
||||||
|
self::assertFalse($closure(new TestObject(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkNotEqualsComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->neq('foo', 1));
|
||||||
|
|
||||||
|
self::assertFalse($closure(new TestObject(1)));
|
||||||
|
self::assertTrue($closure(new TestObject(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkLessThanComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->lt('foo', 1));
|
||||||
|
|
||||||
|
self::assertFalse($closure(new TestObject(1)));
|
||||||
|
self::assertTrue($closure(new TestObject(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkLessThanEqualsComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->lte('foo', 1));
|
||||||
|
|
||||||
|
self::assertFalse($closure(new TestObject(2)));
|
||||||
|
self::assertTrue($closure(new TestObject(1)));
|
||||||
|
self::assertTrue($closure(new TestObject(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkGreaterThanEqualsComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->gte('foo', 1));
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject(2)));
|
||||||
|
self::assertTrue($closure(new TestObject(1)));
|
||||||
|
self::assertFalse($closure(new TestObject(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkGreaterThanComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->gt('foo', 1));
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject(2)));
|
||||||
|
self::assertFalse($closure(new TestObject(1)));
|
||||||
|
self::assertFalse($closure(new TestObject(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkInComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->in('foo', [1, 2, 3, '04']));
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject(2)));
|
||||||
|
self::assertTrue($closure(new TestObject(1)));
|
||||||
|
self::assertFalse($closure(new TestObject(0)));
|
||||||
|
self::assertFalse($closure(new TestObject(4)));
|
||||||
|
self::assertTrue($closure(new TestObject('04')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkInComparisonObjects(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->in('foo', [new TestObject(1), new TestObject(2), new TestObject(4)]));
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject(new TestObject(2))));
|
||||||
|
self::assertTrue($closure(new TestObject(new TestObject(1))));
|
||||||
|
self::assertFalse($closure(new TestObject(new TestObject(0))));
|
||||||
|
self::assertTrue($closure(new TestObject(new TestObject(4))));
|
||||||
|
self::assertFalse($closure(new TestObject(new TestObject('baz'))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkNotInComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->notIn('foo', [1, 2, 3, '04']));
|
||||||
|
|
||||||
|
self::assertFalse($closure(new TestObject(1)));
|
||||||
|
self::assertFalse($closure(new TestObject(2)));
|
||||||
|
self::assertTrue($closure(new TestObject(0)));
|
||||||
|
self::assertTrue($closure(new TestObject(4)));
|
||||||
|
self::assertFalse($closure(new TestObject('04')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkNotInComparisonObjects(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->notIn('foo', [new TestObject(1), new TestObject(2), new TestObject(4)]));
|
||||||
|
|
||||||
|
self::assertFalse($closure(new TestObject(new TestObject(1))));
|
||||||
|
self::assertFalse($closure(new TestObject(new TestObject(2))));
|
||||||
|
self::assertTrue($closure(new TestObject(new TestObject(0))));
|
||||||
|
self::assertFalse($closure(new TestObject(new TestObject(4))));
|
||||||
|
self::assertTrue($closure(new TestObject(new TestObject('baz'))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkContainsComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->contains('foo', 'hello'));
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject('hello world')));
|
||||||
|
self::assertFalse($closure(new TestObject('world')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkMemberOfComparisonWithObject(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->memberof('foo', 2));
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject([1, 2, 3])));
|
||||||
|
self::assertTrue($closure(new TestObject([2])));
|
||||||
|
self::assertTrue($closure(new TestObject(new ArrayIterator([2]))));
|
||||||
|
self::assertFalse($closure(new TestObject([1, 3, 5])));
|
||||||
|
self::assertFalse($closure(new TestObject([1, '02'])));
|
||||||
|
self::assertFalse($closure(new TestObject(new ArrayIterator([4]))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkStartsWithComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->startsWith('foo', 'hello'));
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject('hello world')));
|
||||||
|
self::assertFalse($closure(new TestObject('world')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkEndsWithComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->endsWith('foo', 'world'));
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject('hello world')));
|
||||||
|
self::assertFalse($closure(new TestObject('hello')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkUnknownOperatorComparisonThrowException(): void
|
||||||
|
{
|
||||||
|
self::expectException(RuntimeException::class);
|
||||||
|
self::expectExceptionMessage('Unknown comparison operator: unknown');
|
||||||
|
|
||||||
|
$closure = $this->visitor->walkComparison(new Comparison('foo', 'unknown', 2));
|
||||||
|
|
||||||
|
$closure(new TestObject(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkAndCompositeExpression(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkCompositeExpression(
|
||||||
|
$this->builder->andX(
|
||||||
|
$this->builder->eq('foo', 1),
|
||||||
|
$this->builder->eq('bar', 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject(1, 1)));
|
||||||
|
self::assertFalse($closure(new TestObject(1, 0)));
|
||||||
|
self::assertFalse($closure(new TestObject(0, 1)));
|
||||||
|
self::assertFalse($closure(new TestObject(0, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkOrCompositeExpression(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkCompositeExpression(
|
||||||
|
$this->builder->orX(
|
||||||
|
$this->builder->eq('foo', 1),
|
||||||
|
$this->builder->eq('bar', 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject(1, 1)));
|
||||||
|
self::assertTrue($closure(new TestObject(1, 0)));
|
||||||
|
self::assertTrue($closure(new TestObject(0, 1)));
|
||||||
|
self::assertFalse($closure(new TestObject(0, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkOrAndCompositeExpression(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkCompositeExpression(
|
||||||
|
$this->builder->orX(
|
||||||
|
$this->builder->andX(
|
||||||
|
$this->builder->eq('foo', 1),
|
||||||
|
$this->builder->eq('bar', 1),
|
||||||
|
),
|
||||||
|
$this->builder->andX(
|
||||||
|
$this->builder->eq('foo', 2),
|
||||||
|
$this->builder->eq('bar', 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject(1, 1)));
|
||||||
|
self::assertTrue($closure(new TestObject(2, 2)));
|
||||||
|
self::assertFalse($closure(new TestObject(1, 2)));
|
||||||
|
self::assertFalse($closure(new TestObject(2, 1)));
|
||||||
|
self::assertFalse($closure(new TestObject(0, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkAndOrCompositeExpression(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkCompositeExpression(
|
||||||
|
$this->builder->andX(
|
||||||
|
$this->builder->orX(
|
||||||
|
$this->builder->eq('foo', 1),
|
||||||
|
$this->builder->eq('foo', 2),
|
||||||
|
),
|
||||||
|
$this->builder->orX(
|
||||||
|
$this->builder->eq('bar', 3),
|
||||||
|
$this->builder->eq('bar', 4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertTrue($closure(new TestObject(1, 3)));
|
||||||
|
self::assertTrue($closure(new TestObject(1, 4)));
|
||||||
|
self::assertTrue($closure(new TestObject(2, 3)));
|
||||||
|
self::assertTrue($closure(new TestObject(2, 4)));
|
||||||
|
self::assertFalse($closure(new TestObject(1, 0)));
|
||||||
|
self::assertFalse($closure(new TestObject(2, 0)));
|
||||||
|
self::assertFalse($closure(new TestObject(0, 3)));
|
||||||
|
self::assertFalse($closure(new TestObject(0, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkNotCompositeExpression(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkCompositeExpression(
|
||||||
|
$this->builder->not(
|
||||||
|
$this->builder->eq('foo', 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertFalse($closure(new TestObject(1)));
|
||||||
|
self::assertTrue($closure(new TestObject(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWalkUnknownCompositeExpressionThrowException(): void
|
||||||
|
{
|
||||||
|
self::expectException(RuntimeException::class);
|
||||||
|
self::expectExceptionMessage('Unknown composite Unknown');
|
||||||
|
|
||||||
|
$closure = $this->visitor->walkCompositeExpression(
|
||||||
|
new CompositeExpression('Unknown', []),
|
||||||
|
);
|
||||||
|
|
||||||
|
$closure(new TestObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSortByFieldAscending(): void
|
||||||
|
{
|
||||||
|
$objects = [new TestObject('b'), new TestObject('a'), new TestObject('c')];
|
||||||
|
$sort = ClosureExpressionVisitor::sortByField('foo', 1, null, true);
|
||||||
|
|
||||||
|
usort($objects, $sort);
|
||||||
|
|
||||||
|
self::assertEquals('a', $objects[0]->getFoo());
|
||||||
|
self::assertEquals('b', $objects[1]->getFoo());
|
||||||
|
self::assertEquals('c', $objects[2]->getFoo());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSortByFieldDescending(): void
|
||||||
|
{
|
||||||
|
$objects = [new TestObject('b'), new TestObject('a'), new TestObject('c')];
|
||||||
|
$sort = ClosureExpressionVisitor::sortByField('foo', -1, null, true);
|
||||||
|
|
||||||
|
usort($objects, $sort);
|
||||||
|
|
||||||
|
self::assertEquals('c', $objects[0]->getFoo());
|
||||||
|
self::assertEquals('b', $objects[1]->getFoo());
|
||||||
|
self::assertEquals('a', $objects[2]->getFoo());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSortByFieldKeepOrderWhenSameValue(): void
|
||||||
|
{
|
||||||
|
$firstElement = new TestObject('a');
|
||||||
|
$secondElement = new TestObject('a');
|
||||||
|
|
||||||
|
$objects = [$firstElement, $secondElement];
|
||||||
|
$sort = ClosureExpressionVisitor::sortByField('foo', 0, null, true);
|
||||||
|
|
||||||
|
usort($objects, $sort);
|
||||||
|
|
||||||
|
self::assertSame([$firstElement, $secondElement], $objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSortDelegate(): void
|
||||||
|
{
|
||||||
|
$objects = [new TestObject('a', 'c'), new TestObject('a', 'b'), new TestObject('a', 'a')];
|
||||||
|
$sort = ClosureExpressionVisitor::sortByField('bar', 1, null, true);
|
||||||
|
$sort = ClosureExpressionVisitor::sortByField('foo', 1, $sort, true);
|
||||||
|
|
||||||
|
usort($objects, $sort);
|
||||||
|
|
||||||
|
self::assertEquals('a', $objects[0]->getBar());
|
||||||
|
self::assertEquals('b', $objects[1]->getBar());
|
||||||
|
self::assertEquals('c', $objects[2]->getBar());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testArrayComparison(): void
|
||||||
|
{
|
||||||
|
$closure = $this->visitor->walkComparison($this->builder->eq('foo', 42));
|
||||||
|
|
||||||
|
self::assertTrue($closure(['foo' => 42]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestObject
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly mixed $foo = null,
|
||||||
|
private readonly mixed $bar = null,
|
||||||
|
private readonly mixed $baz = null,
|
||||||
|
private readonly mixed $qux = null,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, mixed> $arguments */
|
||||||
|
public function __call(string $name, array $arguments): mixed
|
||||||
|
{
|
||||||
|
if ($name === 'getqux') {
|
||||||
|
return $this->qux;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFoo(): mixed
|
||||||
|
{
|
||||||
|
return $this->foo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBar(): mixed
|
||||||
|
{
|
||||||
|
return $this->bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isBaz(): mixed
|
||||||
|
{
|
||||||
|
return $this->baz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestObjectNotCamelCase
|
||||||
|
{
|
||||||
|
public function __construct(private readonly int|null $foo_bar)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFooBar(): int|null
|
||||||
|
{
|
||||||
|
return $this->foo_bar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestObjectBothCamelCaseAndUnderscore
|
||||||
|
{
|
||||||
|
public function __construct(private readonly int|null $foo_bar = null, private readonly int|null $fooBar = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFooBar(): int|null
|
||||||
|
{
|
||||||
|
return $this->fooBar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestObjectPublicCamelCaseAndPrivateUnderscore
|
||||||
|
{
|
||||||
|
public function __construct(private readonly int|null $foo_bar = null, public int|null $fooBar = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFooBar(): int|null
|
||||||
|
{
|
||||||
|
return $this->fooBar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestObjectBothPublic
|
||||||
|
{
|
||||||
|
public function __construct(public mixed $foo_bar = null, public mixed $fooBar = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFooBar(): mixed
|
||||||
|
{
|
||||||
|
return $this->foo_bar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestObjectBlankGetter
|
||||||
|
{
|
||||||
|
public function __construct(public int|null $fooBar = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fooBar(): int|null
|
||||||
|
{
|
||||||
|
return $this->fooBar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestObjectWithPrivatePropertyInParentClass extends TestObjectPrivatePropertyOnly
|
||||||
|
{
|
||||||
|
}
|
||||||
Reference in new issue
Block a user