From dd6d913ae65aa56221daf4401c31e88eca122ba8 Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 06:21:06 +0200 Subject: [PATCH] Upstream doctrine/collections state of the touched files before 5817703073 --- src/Expr/ClosureExpressionVisitor.php | 272 +++++++++++ tests/ClosureExpressionVisitorTest.php | 594 +++++++++++++++++++++++++ 2 files changed, 866 insertions(+) create mode 100644 src/Expr/ClosureExpressionVisitor.php create mode 100644 tests/ClosureExpressionVisitorTest.php diff --git a/src/Expr/ClosureExpressionVisitor.php b/src/Expr/ClosureExpressionVisitor.php new file mode 100644 index 0000000..a397b4b --- /dev/null +++ b/src/Expr/ClosureExpressionVisitor.php @@ -0,0 +1,272 @@ +$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); + } +} diff --git a/tests/ClosureExpressionVisitorTest.php b/tests/ClosureExpressionVisitorTest.php new file mode 100644 index 0000000..182f783 --- /dev/null +++ b/tests/ClosureExpressionVisitorTest.php @@ -0,0 +1,594 @@ +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 $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 +{ +}