From c9efc1cdee58f23049e42edf776f126090b299e0 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 9 Jan 2023 15:57:48 +0100 Subject: [PATCH 01/15] Fix initializing lazy objects and get rid of "Typed property must not be accessed before initialization" errors --- .../Hydration/SimpleObjectHydrator.php | 4 ++++ lib/Doctrine/ORM/Proxy/ProxyFactory.php | 23 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php index e9063c03660..f6c66b7f3dc 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -165,6 +165,10 @@ protected function hydrateRowData(array $row, array &$result) } } + if (isset($this->_hints[Query::HINT_REFRESH_ENTITY])) { + $this->registerManaged($this->class, $this->_hints[Query::HINT_REFRESH_ENTITY], $data); + } + $uow = $this->_em->getUnitOfWork(); $entity = $uow->createEntity($entityName, $data, $this->_hints); diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 91a2a89521d..1c98f716e74 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -18,6 +18,7 @@ use Doctrine\ORM\Utility\IdentifierFlattener; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Proxy; +use ReflectionProperty; use Symfony\Component\VarExporter\ProxyHelper; use Symfony\Component\VarExporter\VarExporter; @@ -314,17 +315,23 @@ private function generateSkippedProperties(ClassMetadata $class): string $skippedProperties = ['__isCloning' => true]; $identifiers = array_flip($class->getIdentifierFieldNames()); - foreach ($class->getReflectionClass()->getProperties() as $property) { - $name = $property->getName(); + $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; + $reflector = $class->getReflectionClass(); - if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) { - continue; - } + do { + foreach ($reflector->getProperties($filter) as $property) { + $name = $property->getName(); + + if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) { + continue; + } - $prefix = $property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : ($property->isProtected() ? "\0*\0" : ''); + $prefix = $property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : ($property->isProtected() ? "\0*\0" : ''); - $skippedProperties[$prefix . $name] = true; - } + $skippedProperties[$prefix . $name] = true; + } + $filter = ReflectionProperty::IS_PRIVATE; + } while ($reflector = $reflector->getParentClass()); uksort($skippedProperties, 'strnatcmp'); From 3b8692fa4a5be3b9685591c2c42db70e5872afdd Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 22 Dec 2022 12:46:17 -0500 Subject: [PATCH 02/15] add reproducer --- lib/Doctrine/ORM/Proxy/ProxyFactory.php | 13 +++--- .../Tests/Models/GH10336/GH10336Entity.php | 27 ++++++++++++ .../Tests/Models/GH10336/GH10336Relation.php | 26 +++++++++++ .../ORM/Functional/Ticket/GH10336Test.php | 44 +++++++++++++++++++ 4 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 tests/Doctrine/Tests/Models/GH10336/GH10336Entity.php create mode 100644 tests/Doctrine/Tests/Models/GH10336/GH10336Relation.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/GH10336Test.php diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 1c98f716e74..fe19c7d71e1 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -314,11 +314,10 @@ private function generateSkippedProperties(ClassMetadata $class): string { $skippedProperties = ['__isCloning' => true]; $identifiers = array_flip($class->getIdentifierFieldNames()); + $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; + $reflector = $class->getReflectionClass(); - $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; - $reflector = $class->getReflectionClass(); - - do { + while ($reflector) { foreach ($reflector->getProperties($filter) as $property) { $name = $property->getName(); @@ -330,8 +329,10 @@ private function generateSkippedProperties(ClassMetadata $class): string $skippedProperties[$prefix . $name] = true; } - $filter = ReflectionProperty::IS_PRIVATE; - } while ($reflector = $reflector->getParentClass()); + + $filter = ReflectionProperty::IS_PRIVATE; + $reflector = $reflector->getParentClass(); + } uksort($skippedProperties, 'strnatcmp'); diff --git a/tests/Doctrine/Tests/Models/GH10336/GH10336Entity.php b/tests/Doctrine/Tests/Models/GH10336/GH10336Entity.php new file mode 100644 index 00000000000..9c4fe9da08b --- /dev/null +++ b/tests/Doctrine/Tests/Models/GH10336/GH10336Entity.php @@ -0,0 +1,27 @@ +createSchemaForModels( + GH10336Entity::class, + GH10336Relation::class + ); + } + + public function testCanAccessRelationPropertyAfterClear(): void + { + $relation = new GH10336Relation(); + $relation->value = 'foo'; + $entity = new GH10336Entity(); + $entity->relation = $relation; + + $this->_em->persist($entity); + $this->_em->persist($relation); + $this->_em->flush(); + $this->_em->clear(); + + $entity = $this->_em->find(GH10336Entity::class, 1); + + $this->_em->clear(); + + $this->assertSame('foo', $entity->relation->value); + } +} From 92e63ca4f9ba448059ac21f8be9eab0acd38e2aa Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Fri, 13 Jan 2023 13:33:53 +0000 Subject: [PATCH 03/15] Place a warning about the uses of traits in the documentation --- docs/en/reference/inheritance-mapping.rst | 6 +++ .../limitations-and-known-issues.rst | 41 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/en/reference/inheritance-mapping.rst b/docs/en/reference/inheritance-mapping.rst index e19229320ad..a6bd8f0e8fd 100644 --- a/docs/en/reference/inheritance-mapping.rst +++ b/docs/en/reference/inheritance-mapping.rst @@ -25,6 +25,12 @@ appear in the middle of an otherwise mapped inheritance hierarchy For further support of inheritance, the single or joined table inheritance features have to be used. +.. note:: + + You may be tempted to use traits to mix mapped fields or relationships + into your entity classes to circumvent some of the limitations of + mapped superclasses. Before doing that, please read the section on traits + in the :doc:`Limitations and Known Issues ` chapter. Example: diff --git a/docs/en/reference/limitations-and-known-issues.rst b/docs/en/reference/limitations-and-known-issues.rst index fa0f2be094f..c83c3bd8d4b 100644 --- a/docs/en/reference/limitations-and-known-issues.rst +++ b/docs/en/reference/limitations-and-known-issues.rst @@ -130,10 +130,49 @@ included in the core of Doctrine ORM. However there are already two extensions out there that offer support for Nested Set with ORM: - - `Doctrine2 Hierarchical-Structural Behavior `_ - `Doctrine2 NestedSet `_ +Using Traits in Entity Classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There is no real "official" statement whether or to which extent traits +are supported in entity or mapped superclasses. + +Traits were added in PHP 5.4 more than 10 years ago, but at the same time +more than two years after the initial Doctrine 2 release and the time where +core components were designed. + +In fact, this documentation mentions traits only in the context of +:doc:`overriding field association mappings in subclasses `. Also, the +tests in the ORM codebase (as of writing) cover traits only in two (!) places, +namely the aforementioned override and in an edge-case for the deprecated +"Entity Generator". + +Thus, you should at least be aware that when using traits in your entity and +mapped superclasses, you will be in uncharted terrain. + +.. warning:: + + There be dragons. + +From a more technical point of view, traits basically work at the language level +as if the code contained in them had been copied into the class where the trait +is used, and even private fields are accessible by the using class. In addition to +that, some precedence and conflict resolution rules apply. + +When it comes to loading mapping configuration, the annotation and attribute drivers +rely on PHP reflection to inspect class properties including their docblocks. +As long as the results are consistent with what a solution _without_ traits would +have produced, this is probably fine. + +However, to mention known limitations, it is currently not possible to use "class" level +`annotations ` or `attributes ` on traits, and attempts to improve parser support +for traits as `here ` or `there ` have been abandoned. + +XML mapping configuration probably needs to completely re-configure or otherwise +copy-and-paste configuration for fields used from traits. + Known Issues ------------ From f72f6b199bc79de506433c0356bcb3d4e2e14242 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Fri, 13 Jan 2023 23:31:54 +0100 Subject: [PATCH 04/15] Document the meanings of 'inherited' and 'declared' in field mapping information --- .../ORM/Mapping/ClassMetadataFactory.php | 1 - .../ORM/Mapping/ClassMetadataInfo.php | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index cc0fcd8c610..495218e5550 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -392,7 +392,6 @@ private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $p $mapping['sourceEntity'] = $subClass->name; } - //$subclassMapping = $mapping; if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) { $mapping['inherited'] = $parentClass->name; } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index eec5b8aa3f3..d10af54d449 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -406,6 +406,22 @@ class ClassMetadataInfo implements ClassMetadata /** * READ-ONLY: The names of all embedded classes based on properties. * + * The value (definition) array may contain, among others, the following values: + * + * - 'inherited' (string, optional) + * This is set when this embedded-class field is inherited by this class from another (inheritance) parent + * entity class. The value is the FQCN of the topmost entity class that contains + * mapping information for this field. (If there are transient classes in the + * class hierarchy, these are ignored, so the class property may in fact come + * from a class further up in the PHP class hierarchy.) + * Fields initially declared in mapped superclasses are + * not considered 'inherited' in the nearest entity subclasses. + * + * - 'declared' (string, optional) + * This is set when the embedded-class field does not appear for the first time in this class, but is originally + * declared in another parent entity or mapped superclass. The value is the FQCN + * of the topmost non-transient class that contains mapping information for this field. + * * @psalm-var array */ public $embeddedClasses = []; @@ -523,6 +539,20 @@ class ClassMetadataInfo implements ClassMetadata * - 'unique' (string, optional, schema-only) * Whether a unique constraint should be generated for the column. * + * - 'inherited' (string, optional) + * This is set when the field is inherited by this class from another (inheritance) parent + * entity class. The value is the FQCN of the topmost entity class that contains + * mapping information for this field. (If there are transient classes in the + * class hierarchy, these are ignored, so the class property may in fact come + * from a class further up in the PHP class hierarchy.) + * Fields initially declared in mapped superclasses are + * not considered 'inherited' in the nearest entity subclasses. + * + * - 'declared' (string, optional) + * This is set when the field does not appear for the first time in this class, but is originally + * declared in another parent entity or mapped superclass. The value is the FQCN + * of the topmost non-transient class that contains mapping information for this field. + * * @var mixed[] * @psalm-var array */ @@ -625,6 +655,11 @@ class ClassMetadataInfo implements ClassMetadata * - fieldName (string) * The name of the field in the entity the association is mapped to. * + * - sourceEntity (string) + * The class name of the source entity. In the case of to-many-associations initially + * present in mapped superclasses, the nearest entity subclasses will be + * considered the respective source entities. + * * - targetEntity (string) * The class name of the target entity. If it is fully-qualified it is used as is. * If it is a simple, unqualified class name the namespace is assumed to be the same @@ -661,6 +696,20 @@ class ClassMetadataInfo implements ClassMetadata * This field HAS to be either the primary key or a unique column. Otherwise the collection * does not contain all the entities that are actually related. * + * - 'inherited' (string, optional) + * This is set when the association is inherited by this class from another (inheritance) parent + * entity class. The value is the FQCN of the topmost entity class that contains + * this association. (If there are transient classes in the + * class hierarchy, these are ignored, so the class property may in fact come + * from a class further up in the PHP class hierarchy.) + * To-many associations initially declared in mapped superclasses are + * not considered 'inherited' in the nearest entity subclasses. + * + * - 'declared' (string, optional) + * This is set when the association does not appear in the current class for the first time, but + * is initially declared in another parent entity or mapped superclass. The value is the FQCN + * of the topmost non-transient class that contains association information for this relationship. + * * A join table definition has the following structure: *
      * array(

From 227f60c83276661b2b467b8d4af56eb197111fb0 Mon Sep 17 00:00:00 2001
From: Matthias Pigulla 
Date: Fri, 13 Jan 2023 23:51:39 +0100
Subject: [PATCH 05/15] Update lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Grégoire Paris 
---
 lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
index d10af54d449..bc30da69ec7 100644
--- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
+++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -656,7 +656,7 @@ class ClassMetadataInfo implements ClassMetadata
      * The name of the field in the entity the association is mapped to.
      *
      * - sourceEntity (string)
-     * The class name of the source entity. In the case of to-many-associations initially
+     * The class name of the source entity. In the case of to-many associations initially
      * present in mapped superclasses, the nearest entity subclasses will be
      * considered the respective source entities.
      *

From 39a434914d3f020ab99047a4d7673e51b09c2c3b Mon Sep 17 00:00:00 2001
From: Matthias Pigulla 
Date: Fri, 13 Jan 2023 23:04:40 +0000
Subject: [PATCH 06/15] Be more vague about the Entity Generator

---
 docs/en/reference/limitations-and-known-issues.rst | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/en/reference/limitations-and-known-issues.rst b/docs/en/reference/limitations-and-known-issues.rst
index c83c3bd8d4b..a4b7ef79d8d 100644
--- a/docs/en/reference/limitations-and-known-issues.rst
+++ b/docs/en/reference/limitations-and-known-issues.rst
@@ -146,8 +146,8 @@ core components were designed.
 In fact, this documentation mentions traits only in the context of
 :doc:`overriding field association mappings in subclasses `. Also, the
 tests in the ORM codebase (as of writing) cover traits only in two (!) places,
-namely the aforementioned override and in an edge-case for the deprecated
-"Entity Generator".
+namely the aforementioned override and in an edge-case for a tool that will be
+removed in Doctrine 3.0.
 
 Thus, you should at least be aware that when using traits in your entity and
 mapped superclasses, you will be in uncharted terrain.

From fdccfbd120a2342ba96e687945de79b30f8231eb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= 
Date: Sat, 14 Jan 2023 16:41:18 +0100
Subject: [PATCH 07/15] Stop allowing phpbench's master branch

A stable version has been published, it allows doctrine/annotations 2
---
 composer.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/composer.json b/composer.json
index 32c8ed796e8..34cecb1a756 100644
--- a/composer.json
+++ b/composer.json
@@ -41,7 +41,7 @@
     "require-dev": {
         "doctrine/annotations": "^1.13 || ^2",
         "doctrine/coding-standard": "^9.0.2 || ^11.0",
-        "phpbench/phpbench": "^0.16.10 || ^1.0 || dev-master",
+        "phpbench/phpbench": "^0.16.10 || ^1.0",
         "phpstan/phpstan": "~1.4.10 || 1.9.8",
         "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
         "psr/log": "^1 || ^2 || ^3",

From 853e80ca98c9cf173fa5aa02bcdbb6c9342d6454 Mon Sep 17 00:00:00 2001
From: Matthias Pigulla 
Date: Sat, 14 Jan 2023 17:38:23 +0000
Subject: [PATCH 08/15] Reword text

---
 .../limitations-and-known-issues.rst          | 20 ++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/docs/en/reference/limitations-and-known-issues.rst b/docs/en/reference/limitations-and-known-issues.rst
index a4b7ef79d8d..bf1cb59d268 100644
--- a/docs/en/reference/limitations-and-known-issues.rst
+++ b/docs/en/reference/limitations-and-known-issues.rst
@@ -136,18 +136,17 @@ ORM:
 Using Traits in Entity Classes
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-There is no real "official" statement whether or to which extent traits
-are supported in entity or mapped superclasses.
+The use of traits in entity or mapped superclasses, at least when they
+include mapping configuration or mapped fields, is currently not
+endorsed by the Doctrine project. The reasons for this are as follows.
 
 Traits were added in PHP 5.4 more than 10 years ago, but at the same time
 more than two years after the initial Doctrine 2 release and the time where
 core components were designed.
 
 In fact, this documentation mentions traits only in the context of
-:doc:`overriding field association mappings in subclasses `. Also, the
-tests in the ORM codebase (as of writing) cover traits only in two (!) places,
-namely the aforementioned override and in an edge-case for a tool that will be
-removed in Doctrine 3.0.
+:doc:`overriding field association mappings in subclasses `.
+Coverage of traits in test cases is practically nonexistent.
 
 Thus, you should at least be aware that when using traits in your entity and
 mapped superclasses, you will be in uncharted terrain.
@@ -166,9 +165,12 @@ rely on PHP reflection to inspect class properties including their docblocks.
 As long as the results are consistent with what a solution _without_ traits would
 have produced, this is probably fine.
 
-However, to mention known limitations, it is currently not possible to use "class" level
-`annotations ` or `attributes ` on traits, and attempts to improve parser support
-for traits as `here ` or `there ` have been abandoned.
+However, to mention known limitations, it is currently not possible to use "class"
+level `annotations ` or
+`attributes ` on traits, and attempts to
+improve parser support for traits as `here `
+or `there ` have been abandoned
+due to complexity.
 
 XML mapping configuration probably needs to completely re-configure or otherwise
 copy-and-paste configuration for fields used from traits.

From b56de5b0e2d687e90222100930c61dabf7de07c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= 
Date: Sun, 15 Jan 2023 20:33:55 +0100
Subject: [PATCH 09/15] Type TypedFieldMapper API more precisely

---
 lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php | 4 ++--
 lib/Doctrine/ORM/Mapping/TypedFieldMapper.php  | 5 +++--
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
index eec5b8aa3f3..35628097621 100644
--- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
+++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -1582,9 +1582,9 @@ private function isTypedProperty(string $name): bool
     /**
      * Validates & completes the given field mapping based on typed property.
      *
-     * @param  array{fieldName: string, type?: mixed} $mapping The field mapping to validate & complete.
+     * @param  array{fieldName: string, type?: string} $mapping The field mapping to validate & complete.
      *
-     * @return array{fieldName: string, enumType?: string, type?: mixed} The updated mapping.
+     * @return array{fieldName: string, enumType?: class-string, type?: string} The updated mapping.
      */
     private function validateAndCompleteTypedFieldMapping(array $mapping): array
     {
diff --git a/lib/Doctrine/ORM/Mapping/TypedFieldMapper.php b/lib/Doctrine/ORM/Mapping/TypedFieldMapper.php
index faf84dbd755..2db9e903dfa 100644
--- a/lib/Doctrine/ORM/Mapping/TypedFieldMapper.php
+++ b/lib/Doctrine/ORM/Mapping/TypedFieldMapper.php
@@ -4,6 +4,7 @@
 
 namespace Doctrine\ORM\Mapping;
 
+use BackedEnum;
 use ReflectionProperty;
 
 interface TypedFieldMapper
@@ -11,9 +12,9 @@ interface TypedFieldMapper
     /**
      * Validates & completes the given field mapping based on typed property.
      *
-     * @param array{fieldName: string, enumType?: string, type?: mixed} $mapping The field mapping to validate & complete.
+     * @param array{fieldName: string, enumType?: class-string, type?: string} $mapping The field mapping to validate & complete.
      *
-     * @return array{fieldName: string, enumType?: string, type?: mixed} The updated mapping.
+     * @return array{fieldName: string, enumType?: class-string, type?: string} The updated mapping.
      */
     public function validateAndComplete(array $mapping, ReflectionProperty $field): array;
 }

From 843b0fcc16ad0c0407416756c4a523b7bd160e7b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= 
Date: Sat, 14 Jan 2023 21:14:41 +0100
Subject: [PATCH 10/15] Add missing fields

---
 lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php | 4 +++-
 phpstan-baseline.neon                          | 5 -----
 psalm-baseline.xml                             | 3 ---
 3 files changed, 3 insertions(+), 9 deletions(-)

diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
index 35628097621..0f8977dc4c3 100644
--- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
+++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -90,7 +90,9 @@
  *      requireSQLConversion?: bool,
  *      declared?: class-string,
  *      declaredField?: string,
- *      options?: array
+ *      options?: array,
+ *      version?: string,
+ *      default?: string|int,
  * }
  * @psalm-type JoinColumnData = array{
  *     name: string,
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 37698eed36c..046d7eb52c7 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -575,11 +575,6 @@ parameters:
 			count: 1
 			path: lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php
 
-		-
-			message: "#^Offset 'version' on array\\{type\\: string, fieldName\\: string, columnName\\: string, length\\?\\: int, id\\?\\: bool, nullable\\?\\: bool, notInsertable\\?\\: bool, notUpdatable\\?\\: bool, \\.\\.\\.\\} in isset\\(\\) does not exist\\.$#"
-			count: 1
-			path: lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php
-
 		-
 			message: "#^Parameter \\#1 \\$policy of method Doctrine\\\\ORM\\\\Tools\\\\Export\\\\Driver\\\\AbstractExporter\\:\\:_getChangeTrackingPolicyString\\(\\) expects 1\\|2\\|3, int given\\.$#"
 			count: 1
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 97aded1a4c7..3e74d9245df 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -2694,9 +2694,6 @@
     
       AbstractExporter
     
-    
-      $field['version']
-    
     
       $_extension
     

From c0a7317e8d7d4abde6416da515783a7a0ff12d8e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= 
Date: Sun, 15 Jan 2023 20:16:21 +0100
Subject: [PATCH 11/15] Reuse array shape

---
 lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
index 0f8977dc4c3..78c13517e0e 100644
--- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
+++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -1630,7 +1630,7 @@ private function validateAndCompleteTypedAssociationMapping(array $mapping): arr
      *     enumType?: class-string,
      * } $mapping The field mapping to validate & complete.
      *
-     * @return mixed[] The updated mapping.
+     * @return FieldMapping The updated mapping.
      *
      * @throws MappingException
      */

From a8ef69dbe6153593e09c256361272068aa82c288 Mon Sep 17 00:00:00 2001
From: Matthias Pigulla 
Date: Mon, 16 Jan 2023 03:33:54 +0100
Subject: [PATCH 12/15] Factor out logic that tracks mapping inheritance
 (#10397)

---
 .../ORM/Mapping/ClassMetadataFactory.php      | 45 +++++++++----------
 1 file changed, 20 insertions(+), 25 deletions(-)

diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
index cc0fcd8c610..9a54cf6459b 100644
--- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
+++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
@@ -354,20 +354,30 @@ private function getShortName(string $className): string
         return strtolower(end($parts));
     }
 
+    /**
+     * Puts the `inherited` and `declared` values into mapping information for fields, associations
+     * and embedded classes.
+     *
+     * @param mixed[] $mapping
+     */
+    private function addMappingInheritanceInformation(array &$mapping, ClassMetadata $parentClass): void
+    {
+        if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
+            $mapping['inherited'] = $parentClass->name;
+        }
+
+        if (! isset($mapping['declared'])) {
+            $mapping['declared'] = $parentClass->name;
+        }
+    }
+
     /**
      * Adds inherited fields to the subclass mapping.
      */
     private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass): void
     {
         foreach ($parentClass->fieldMappings as $mapping) {
-            if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
-                $mapping['inherited'] = $parentClass->name;
-            }
-
-            if (! isset($mapping['declared'])) {
-                $mapping['declared'] = $parentClass->name;
-            }
-
+            $this->addMappingInheritanceInformation($mapping, $parentClass);
             $subClass->addInheritedFieldMapping($mapping);
         }
 
@@ -392,15 +402,7 @@ private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $p
                 $mapping['sourceEntity'] = $subClass->name;
             }
 
-            //$subclassMapping = $mapping;
-            if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
-                $mapping['inherited'] = $parentClass->name;
-            }
-
-            if (! isset($mapping['declared'])) {
-                $mapping['declared'] = $parentClass->name;
-            }
-
+            $this->addMappingInheritanceInformation($mapping, $parentClass);
             $subClass->addInheritedAssociationMapping($mapping);
         }
     }
@@ -408,14 +410,7 @@ private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $p
     private function addInheritedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass): void
     {
         foreach ($parentClass->embeddedClasses as $field => $embeddedClass) {
-            if (! isset($embeddedClass['inherited']) && ! $parentClass->isMappedSuperclass) {
-                $embeddedClass['inherited'] = $parentClass->name;
-            }
-
-            if (! isset($embeddedClass['declared'])) {
-                $embeddedClass['declared'] = $parentClass->name;
-            }
-
+            $this->addMappingInheritanceInformation($embeddedClass, $parentClass);
             $subClass->embeddedClasses[$field] = $embeddedClass;
         }
     }

From f0616626e0452c56a914a70a263842bb9fa2d327 Mon Sep 17 00:00:00 2001
From: "Alexander M. Turek" 
Date: Mon, 16 Jan 2023 15:53:19 +0700
Subject: [PATCH 13/15] Test with a stable PHPUnit (#10406)

---
 composer.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/composer.json b/composer.json
index da2a6d27679..84574778509 100644
--- a/composer.json
+++ b/composer.json
@@ -43,7 +43,7 @@
         "doctrine/coding-standard": "^9.0.2 || ^11.0",
         "phpbench/phpbench": "^0.16.10 || ^1.0",
         "phpstan/phpstan": "~1.4.10 || 1.9.8",
-        "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5.28@dev",
+        "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5.28",
         "psr/log": "^1 || ^2 || ^3",
         "squizlabs/php_codesniffer": "3.7.1",
         "symfony/cache": "^4.4 || ^5.4 || ^6.0",

From b2e42dc92d82c298724fce86ee853eed199cdd1a Mon Sep 17 00:00:00 2001
From: Thomas Landauer 
Date: Mon, 16 Jan 2023 17:19:29 +0100
Subject: [PATCH 14/15] Adding link to Attributes reference

In fact, I moved it upwards, and updated it to new target :-)
---
 docs/en/reference/improving-performance.rst | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/docs/en/reference/improving-performance.rst b/docs/en/reference/improving-performance.rst
index ee74e6594f9..79610644a7c 100644
--- a/docs/en/reference/improving-performance.rst
+++ b/docs/en/reference/improving-performance.rst
@@ -45,8 +45,7 @@ in scenarios where data is loaded for read-only purposes.
 Read-Only Entities
 ------------------
 
-You can mark entities as read only (See metadata mapping
-references for details).
+You can mark entities as read only. For details, see :ref:`attrref_entity`
 
 This means that the entity marked as read only is never considered for updates.
 During flush on the EntityManager these entities are skipped even if properties
@@ -55,8 +54,6 @@ changed.
 Read-Only allows to persist new entities of a kind and remove existing ones,
 they are just not considered for updates.
 
-See :ref:`annref_entity`
-
 You can also explicitly mark individual entities read only directly on the
 UnitOfWork via a call to ``markReadOnly()``:
 

From 37572802ccd6913d7cb033fed949fc998e34893e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= 
Date: Tue, 17 Jan 2023 11:27:42 +0100
Subject: [PATCH 15/15] Reuse association mapping array shape (#10403)

---
 lib/Doctrine/ORM/ORMInvalidArgumentException.php | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/lib/Doctrine/ORM/ORMInvalidArgumentException.php b/lib/Doctrine/ORM/ORMInvalidArgumentException.php
index 62b406a3dd4..4d207f63844 100644
--- a/lib/Doctrine/ORM/ORMInvalidArgumentException.php
+++ b/lib/Doctrine/ORM/ORMInvalidArgumentException.php
@@ -6,6 +6,7 @@
 
 use Doctrine\Deprecations\Deprecation;
 use Doctrine\ORM\Mapping\ClassMetadata;
+use Doctrine\ORM\Mapping\ClassMetadataInfo;
 use InvalidArgumentException;
 
 use function array_map;
@@ -22,6 +23,8 @@
 
 /**
  * Contains exception messages for all invalid lifecycle state exceptions inside UnitOfWork
+ *
+ * @psalm-import-type AssociationMapping from ClassMetadataInfo
  */
 class ORMInvalidArgumentException extends InvalidArgumentException
 {
@@ -109,7 +112,7 @@ static function (array $newEntityWithAssociation): string {
 
     /**
      * @param object $entry
-     * @psalm-param array $associationMapping
+     * @psalm-param AssociationMapping $associationMapping
      *
      * @return ORMInvalidArgumentException
      */
@@ -271,7 +274,7 @@ private static function objToStr($obj): string
 
     /**
      * @param object $entity
-     * @psalm-param array $associationMapping
+     * @psalm-param AssociationMapping $associationMapping
      */
     private static function newEntityFoundThroughRelationshipMessage(array $associationMapping, $entity): string
     {