From a3ec81dc3862238179c3ca740d00690a5d970abc Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 19 Oct 2026 13:17:03 +0200 Subject: [PATCH] doctrine/dbal:^4 and doctrine/collections:^2 compatibility --- composer.json | 6 +- src/Collection/ImmutableCollection.php | 20 ++++++- .../DoctrineMappingProcessor.php | 16 +++-- test/Collection/ImmutableCollectionTest.php | 25 +++++++- .../DoctrineMappingProcessorTest.php | 60 +++++++------------ 5 files changed, 79 insertions(+), 48 deletions(-) diff --git a/composer.json b/composer.json index da9df4e..c17edee 100644 --- a/composer.json +++ b/composer.json @@ -9,10 +9,10 @@ "composer-plugin-api": "^2.0", "ext-bcmath": "*", "ext-json": "*", - "doctrine/collections": "^1.8.0", - "doctrine/dbal": "^3.7.2", + "doctrine/collections": "^1.8.0||^2.0", + "doctrine/dbal": "^3.7.2||^4.0", "doctrine/inflector": "^2.0.8", - "doctrine/orm": "^2.17.2", + "doctrine/orm": "^2.17.2||^3.0", "symfony/filesystem": "^6.0||^7.0", "symfony/finder": "^6.0||^7.0", "twig/twig": "^3.15.0" diff --git a/src/Collection/ImmutableCollection.php b/src/Collection/ImmutableCollection.php index 2db321f..d5d10b5 100644 --- a/src/Collection/ImmutableCollection.php +++ b/src/Collection/ImmutableCollection.php @@ -46,7 +46,9 @@ public function __construct(Collection $collection) public function add($element): bool { if ($this->is_clone) { - return $this->collection->add($element); + $this->collection->add($element); + + return true; } throw new \LogicException('This collection is immutable'); @@ -209,6 +211,22 @@ public function filter(Closure $predicate): Collection return $this->collection->filter($predicate); } + /** + * Only part of the ReadableCollection interface as of doctrine/collections 2.1. + */ + public function findFirst(Closure $predicate): mixed + { + return $this->collection->findFirst($predicate); + } + + /** + * Only part of the ReadableCollection interface as of doctrine/collections 2.1. + */ + public function reduce(Closure $func, mixed $initial = null): mixed + { + return $this->collection->reduce($func, $initial); + } + public function forAll(Closure $predicate): bool { return $this->collection->forAll($predicate); diff --git a/src/PropertyProcessor/DoctrineMappingProcessor.php b/src/PropertyProcessor/DoctrineMappingProcessor.php index 657be92..765bb26 100644 --- a/src/PropertyProcessor/DoctrineMappingProcessor.php +++ b/src/PropertyProcessor/DoctrineMappingProcessor.php @@ -25,7 +25,13 @@ class DoctrineMappingProcessor implements PropertyProcessorInterface { private const string ZEROED_DATE_TIME = 'zeroeddatetime'; private const string ZEROED_DATE = 'zeroeddate'; - private const string YAML_ARRAY = 'yaml_array'; + /** + * The 'array' and 'object' types are deprecated since doctrine/dbal:3.4 and removed in + * doctrine/dbal:4.0. + */ + private const string ARRAY = 'array'; + private const string OBJECT = 'object'; + private const string YAML_ARRAY = 'yaml_array'; /** * @deprecated since doctrine/dbal:2.6 */ @@ -114,7 +120,7 @@ private function processBidirectional(object $attribute, PropertyInformation $in // Set default value for nullable. if ($information->isNullable() === null) { - $information->setNullable((new JoinColumn())->nullable); + $information->setNullable(true); } // Set field name for index on this collection. @@ -171,7 +177,7 @@ protected function processColumn(Column $column, PropertyInformation $informatio */ private function processJoinColumn(JoinColumn $join_column, PropertyInformation $information): void { - $information->setNullable($join_column->nullable); + $information->setNullable($join_column->nullable ?? true); $information->setUnique($join_column->unique); } @@ -251,13 +257,13 @@ private function transformType($type) if (\in_array( $type, - [Types::SIMPLE_ARRAY, self::JSON_ARRAY, Types::JSON, Types::ARRAY, self::YAML_ARRAY], + [Types::SIMPLE_ARRAY, self::JSON_ARRAY, Types::JSON, self::ARRAY, self::YAML_ARRAY], true )) { return 'array'; } - if ($type === Types::OBJECT) { + if ($type === self::OBJECT) { return 'object'; } diff --git a/test/Collection/ImmutableCollectionTest.php b/test/Collection/ImmutableCollectionTest.php index 893479d..7b3d6ad 100644 --- a/test/Collection/ImmutableCollectionTest.php +++ b/test/Collection/ImmutableCollectionTest.php @@ -70,7 +70,8 @@ public function testCompatibility(): void self::assertSame($col->slice(2, 2), $imm->slice(2, 2)); self::assertSame($col->next(), $imm->current()); self::assertSame($imm->next(), $col->current()); - self::assertSame($col->add('a'), $cln->add('a')); + $col->add('a'); + self::assertTrue($cln->add('a')); self::assertSame($col->remove(0), $cln->remove(0)); self::assertSame($col->removeElement('a'), $cln->removeElement('a')); } @@ -262,4 +263,26 @@ public function testMatching(): void // Test that we got a copy and did not change the original collection. self::assertEquals($before, $this->immutable_collection->toArray()); } + + public function testFindFirst(): void + { + if (!method_exists(ArrayCollection::class, 'findFirst')) { + self::markTestSkipped('findFirst() requires doctrine/collections 2.1 or higher.'); + } + + self::assertSame(5, $this->immutable_collection->findFirst(static fn ($key, $value) => $value > 3)); + self::assertNull($this->immutable_collection->findFirst(static fn ($key, $value) => $value > 100)); + } + + public function testReduce(): void + { + if (!method_exists(ArrayCollection::class, 'reduce')) { + self::markTestSkipped('reduce() requires doctrine/collections 2.1 or higher.'); + } + + $sum = static fn ($carry, $value) => $carry + $value; + + self::assertSame(33, $this->immutable_collection->reduce($sum, 0)); + self::assertNull((new ImmutableCollection(new ArrayCollection()))->reduce($sum)); + } } diff --git a/test/PropertyProcessor/DoctrineMappingProcessorTest.php b/test/PropertyProcessor/DoctrineMappingProcessorTest.php index 1eed646..8b71fef 100644 --- a/test/PropertyProcessor/DoctrineMappingProcessorTest.php +++ b/test/PropertyProcessor/DoctrineMappingProcessorTest.php @@ -48,39 +48,38 @@ protected function setUp(): void public function columnProvider(): iterable { $property = new ReflectionProperty('test'); - $implicit = new Column(null, 'string'); - $explicit = new Column(null, 'string'); - $faulty = new Column(null, 'string'); + $implicit = new Column(type: 'string'); + $explicit = new Column( + type: Types::BIGINT, + length: 100, + precision: 3, + scale: 9, + unique: true, + nullable: true + ); + $faulty = new Column(type: Types::DECIMAL); $implicit_info = new PropertyInformation($property); $explicit_info = new PropertyInformation($property); $faulty_info = new PropertyInformation($property); $implicit_info->setType($implicit->type); - $explicit_info->setType($explicit->type); - $faulty_info->setType($faulty->type); + $explicit_info->setType('string'); + $faulty_info->setType('string'); - $explicit->length = 100; $explicit_info->setLength(100); - $explicit->nullable = 'jumbo_dumbo'; $explicit_info->setNullable(true); - $explicit->precision = 3; $explicit_info->setPrecision(3); - $explicit->scale = 9; $explicit_info->setScale(9); - $explicit->type = Types::BIGINT; $explicit_info->setType('integer'); $explicit_info->setIntegerSize(64); - $explicit->unique = true; $explicit_info->setUnique(true); - $faulty->type = Types::DECIMAL; - return [ [$implicit, $implicit_info, null], [$explicit, $explicit_info, null], @@ -153,27 +152,15 @@ public function testProcessColumn(Column $column, PropertyInformation $output, $ public function associationProvider(): iterable { - $many_to_many = new ManyToMany(); - $many_to_one = new ManyToOne(); - $one_to_many = new OneToMany(); - $one_to_one = new OneToOne(); + $many_to_many = new ManyToMany(targetEntity: '\\Employee'); + $many_to_one = new ManyToOne(targetEntity: 'Employee'); + $one_to_many = new OneToMany(targetEntity: '\\Employer'); + $one_to_one = new OneToOne(targetEntity: 'Boss'); $generated_value = new GeneratedValue(); - $join_column = new JoinColumn(); - $one_to_one_bi = new OneToOne(); - $one_to_many_bi = new OneToMany(); - $one_to_many_index = new OneToMany(); - - $many_to_many->targetEntity = '\\Employee'; - $many_to_one->targetEntity = 'Employee'; - $one_to_many->targetEntity = '\\Employer'; - $one_to_one->targetEntity = 'Boss'; - $join_column->name = 'id'; - $one_to_one_bi->inversedBy = 'id'; - $one_to_one_bi->targetEntity = 'Employee\\Boss'; - $one_to_many_bi->mappedBy = 'id'; - $one_to_many_bi->targetEntity = 'LunchLady'; - $one_to_many_index->indexBy = 'index'; - $one_to_many_index->targetEntity = '\\Employee'; + $join_column = new JoinColumn(name: 'id'); + $one_to_one_bi = new OneToOne(targetEntity: 'Employee\\Boss', inversedBy: 'id'); + $one_to_many_bi = new OneToMany(targetEntity: 'LunchLady', mappedBy: 'id'); + $one_to_many_index = new OneToMany(targetEntity: '\\Employee', indexBy: 'index'); return [ [$many_to_many], @@ -298,12 +285,9 @@ public function testTypeConversion($doctrine_type, $php_type, $exception = ''): && $doctrine_type && (ctype_upper($doctrine_type[0]) || $doctrine_type[0] === '\\') ) { - $attribute = new ManyToOne(); - $attribute->targetEntity = $doctrine_type; + $attribute = new ManyToOne(targetEntity: $doctrine_type); } else { - $attribute = new Column(); - $attribute->type = $doctrine_type; - $attribute->scale = 1; + $attribute = new Column(type: $doctrine_type, scale: 1); } $this->processor->apply($attribute, $this->information);