Repository: doctrine-extensions/DoctrineExtensions (language: php)

Issue:
ORM 3 deprecations
<!--
    Before you open an issue, make sure this one does not already exist.
    Please, read the contents at https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/CONTRIBUTING.md#what-you-can-contribute.
-->

<!--
    If you are reporting a bug, please try to fill in the following.
    Otherwise remove it.
-->

### Environment

#### Package

<details><summary>show</summary>
<p>

```
$ composer show --latest gedmo/doctrine-extensions

name     : gedmo/doctrine-extensions
descrip. : Doctrine behavioral extensions
keywords : Blameable, behaviors, doctrine, extensions, gedmo, loggable, nestedset, odm, orm, sluggable, sortable, timestampable, translatable, tree, uploadable
versions : * v3.16.0
released : 2024-06-24, this week
latest   : v3.16.0 released 2024-06-24, this week
type     : library
license  : MIT License (MIT) (OSI approved) https://spdx.org/licenses/MIT.html#licenseText
homepage : http://gediminasm.org/
source   : [git] https://github.com/doctrine-extensions/DoctrineExtensions.git 7832a3059bdaf14433a2a2515d533c6a16e58520
dist     : [zip] https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/7832a3059bdaf14433a2a2515d533c6a16e58520 7832a3059bdaf14433a2a2515d533c6a16e58520
path     : /mnt/M.2/Develop/mark/prokat/prokat/vendor/gedmo/doctrine-extensions
names    : gedmo/doctrine-extensions

support
email : gediminas.morkevicius@gmail.com
issues : https://github.com/doctrine-extensions/DoctrineExtensions/issues
source : https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.16.0
wiki : https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc

autoload
psr-4
Gedmo\ => src/

requires
behat/transliterator ^1.2
doctrine/collections ^1.2 || ^2.0
doctrine/common ^2.13 || ^3.0
doctrine/deprecations ^1.0
doctrine/event-manager ^1.2 || ^2.0
doctrine/persistence ^2.2 || ^3.0
php ^7.4 || ^8.0
psr/cache ^1 || ^2 || ^3
psr/clock ^1
symfony/cache ^5.4 || ^6.0 || ^7.0

requires (dev)
doctrine/annotations ^1.13 || ^2.0
doctrine/cache ^1.11 || ^2.0
doctrine/dbal ^3.2
doctrine/doctrine-bundle ^2.3
doctrine/mongodb-odm ^2.3
doctrine/orm ^2.14.0 || ^3.0
friendsofphp/php-cs-fixer ^3.14.0
nesbot/carbon ^2.71 || ^3.0
phpstan/phpstan ^1.11
phpstan/phpstan-doctrine ^1.4
phpstan/phpstan-phpunit ^1.4
phpunit/phpunit ^9.6
rector/rector ^1.1
symfony/console ^5.4 || ^6.0 || ^7.0
symfony/doctrine-bridge ^5.4 || ^6.0 || ^7.0
symfony/phpunit-bridge ^6.0 || ^7.0
symfony/uid ^5.4 || ^6.0 || ^7.0
symfony/yaml ^5.4 || ^6.0 || ^7.0

suggests
doctrine/mongodb-odm to use the extensions with the MongoDB ODM
doctrine/orm to use the extensions with the ORM

conflicts
doctrine/annotations <1.13 || >=3.0
doctrine/dbal <3.2 || >=4.0
doctrine/mongodb-odm <2.3 || >=3.0
doctrine/orm <2.14.0 || 2.16.0 || 2.16.1 || >=4.0
```

</p>
</details>

#### Doctrine packages

<details><summary>show</summary>
<p>

```
$ composer show --latest 'doctrine/*'

Direct dependencies required in composer.json:
doctrine/dbal                       3.8.6  4.0.4  Powerful PHP database abstraction layer...
doctrine/doctrine-bundle            2.12.0 2.12.0 Symfony DoctrineBundle
doctrine/doctrine-migrations-bundle 3.3.1  3.3.1  Symfony DoctrineMigrationsBundle

Transitive dependencies not required in composer.json:
doctrine/cache                      2.2.0  2.2.0  PHP Doctrine Cache library is a popular...
doctrine/collections                2.2.2  2.2.2  PHP Doctrine Collections library that a...
doctrine/common                     3.4.4  3.4.4  PHP Doctrine Common project is a librar...
doctrine/deprecations               1.1.3  1.1.3  A small layer on top of trigger_error(E...
doctrine/event-manager              2.0.1  2.0.1  The Doctrine Event Manager is a simple ...
doctrine/inflector                  2.0.10 2.0.10 PHP Doctrine Inflector is a small libra...
doctrine/instantiator               2.0.0  2.0.0  A small, lightweight utility to instant...
doctrine/lexer                      3.0.1  3.0.1  PHP Doctrine Lexer parser library that ...
doctrine/migrations                 3.7.4  3.7.4  PHP Doctrine Migrations project offer a...
doctrine/orm                        3.2.0  3.2.0  Object-Relational-Mapper for PHP
doctrine/persistence                3.3.3  3.3.3  The Doctrine Persistence project is a s...
doctrine/sql-formatter              1.4.0  1.4.0  a PHP SQL highlighting library
```

</p>
</details>

#### PHP version

```
$ php -v

PHP 8.2.18 (cli) (built: Apr 11 2024 19:20:54) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.18, Copyright (c) Zend Technologies
    with Zend OPcache v8.2.18, Copyright (c), by Zend Technologies
```

## Subject

After update to latest (3.16.0) version which support ORM 3 there are deprecations appear in symfony debug toolbar.

> User Deprecated: Using ArrayAccess on Doctrine\ORM\Mapping\FieldMapping is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead. (ArrayAccessImplementation.php:31 called by AbstractAnnotationDriver.php:134, https://github.com/doctrine/orm/pull/11211, package doctrine/orm)


https://github.com/doctrine-extensions/DoctrineExtensions/blob/c1c245573a3c4081bc626b9132059652a93463fd/src/Tree/Mapping/Validator.php#L100

https://github.com/doctrine-extensions/DoctrineExtensions/blob/c1c245573a3c4081bc626b9132059652a93463fd/src/Mapping/Driver/AbstractAnnotationDriver.php#L134

etc


Discussion / hints:
This is expected behavior right now as a deprecation-free environment would require either dropping ORM 2.x support or adding way too many version checks to the code to avoid them.  You can silence these deprecations through Doctrine's deprecations library with this:

```php
use Doctrine\Deprecations\Deprecation;

// Ignore unfixable Doctrine deprecations
Deprecation::ignoreDeprecations(
    'https://github.com/doctrine/orm/pull/11211', // The ORM changed from arrays to named data objects in 3.x, some packages still use array access for B/C
);
```
Any idea of how to implement this suppression while using StofDoctrineExtensionsBundle?
> either dropping ORM 2.x support or adding way too many version checks to the code to avoid them

Or just search/replace like `$mapping['type']` to `$mapping->type ?? $mapping['type']`
> Any idea of how to implement this suppression while using StofDoctrineExtensionsBundle?

You can do this in `App\Kernel` constructor for example
> > either dropping ORM 2.x support or adding way too many version checks to the code to avoid them
> 
> Or just search/replace like `$mapping['type']` to `$mapping->type ?? $mapping['type']`

While it looks that simple, it adds a lot of maintenance overhead.  A fair number of the uses of the mapping data is in code that runs for both the MongoDB ODM and ORM.  `ClassMetadata::getFieldMapping()` isn't part of the persistence interface, so a consistent type contract doesn't exist.  The ODM and ORM 2.x use an array and ORM 3.x uses an object that implements `ArrayAccess` and triggers deprecations when using array access.  The simplest solution right now is to just leave the deprecated array access in place; eventually, someone is going to need to go through all 38 uses of `$meta->getFieldMapping()` and figure out the context they run in and what the best way to deal with the different type structures is going to be, but given that it's just a deprecation notice and not something that's causing applications to not work (and I get how annoying deprecations can be, I've got dozens of them in the main Symfony application I work on that are unfixable), it's not an extremely urgent thing to address at the moment.
Yeah, I'm so brave decided to make pull request :smile: 
Then I found that it is not so simple. 

Maybe use "BC promise", increase major version and cleanup array access then ?

About ODM - just need to kick they )
> The ODM and ORM 2.x use an array and ORM 3.x uses an object that implements `ArrayAccess` and triggers deprecations when using array access.

I'm probably missing something here, but... doesn't `$mapping->type ?? $mapping['type']` avoid triggering the array access if the property is present, hence avoiding both deprecations?

https://3v4l.org/X018d vs https://3v4l.org/NEdul shows how it's a totally doable approach.
It avoid, but it is not so simple "find & replace"
Need more brave soldier than I )

Fix patch:
diff --git a/doc/mapping.md b/doc/mapping.md
index fe90e2c0d..5cb48cc37 100644
--- a/doc/mapping.md
+++ b/doc/mapping.md
@@ -149,7 +149,7 @@ class Annotation implements Driver
                 }
                 // validate encoding type
                 $mapping = $meta->getFieldMapping($field);
-                if ($mapping['type'] != 'string') {
+                if (($mapping->type ?? $mapping['type']) != 'string') {
                     throw new \Exception("Only strings can be encoded");
                 }
                 // store the metadata
diff --git a/src/Blameable/Mapping/Driver/Xml.php b/src/Blameable/Mapping/Driver/Xml.php
index 42861f8e1..703a5c19a 100644
--- a/src/Blameable/Mapping/Driver/Xml.php
+++ b/src/Blameable/Mapping/Driver/Xml.php
@@ -128,6 +128,6 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true);
     }
 }
diff --git a/src/Blameable/Mapping/Driver/Yaml.php b/src/Blameable/Mapping/Driver/Yaml.php
index e72b33489..02010462f 100644
--- a/src/Blameable/Mapping/Driver/Yaml.php
+++ b/src/Blameable/Mapping/Driver/Yaml.php
@@ -134,6 +134,6 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true);
     }
 }
diff --git a/src/IpTraceable/Mapping/Driver/Xml.php b/src/IpTraceable/Mapping/Driver/Xml.php
index 9e64a9efd..c8759f5c3 100644
--- a/src/IpTraceable/Mapping/Driver/Xml.php
+++ b/src/IpTraceable/Mapping/Driver/Xml.php
@@ -130,6 +130,6 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true);
     }
 }
diff --git a/src/IpTraceable/Mapping/Driver/Yaml.php b/src/IpTraceable/Mapping/Driver/Yaml.php
index 590969ea8..66c2509d2 100644
--- a/src/IpTraceable/Mapping/Driver/Yaml.php
+++ b/src/IpTraceable/Mapping/Driver/Yaml.php
@@ -130,6 +130,6 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true);
     }
 }
diff --git a/src/Loggable/Entity/Repository/LogEntryRepository.php b/src/Loggable/Entity/Repository/LogEntryRepository.php
index 9e3274f80..2769cbe6d 100644
--- a/src/Loggable/Entity/Repository/LogEntryRepository.php
+++ b/src/Loggable/Entity/Repository/LogEntryRepository.php
@@ -165,7 +165,7 @@ protected function mapValue(ClassMetadata $objectMeta, $field, &$value)
         }
 
         $mapping = $objectMeta->getAssociationMapping($field);
-        $value = $value ? $this->getEntityManager()->getReference($mapping['targetEntity'], $value) : null;
+        $value = $value ? $this->getEntityManager()->getReference($mapping->targetEntity ?? $mapping['targetEntity'], $value) : null;
     }
 
     /**
diff --git a/src/Mapping/Driver/AbstractAnnotationDriver.php b/src/Mapping/Driver/AbstractAnnotationDriver.php
index 43c92deae..fb170f366 100644
--- a/src/Mapping/Driver/AbstractAnnotationDriver.php
+++ b/src/Mapping/Driver/AbstractAnnotationDriver.php
@@ -131,7 +131,7 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], $this->validTypes, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], $this->validTypes, true);
     }
 
     /**
diff --git a/src/ReferenceIntegrity/ReferenceIntegrityListener.php b/src/ReferenceIntegrity/ReferenceIntegrityListener.php
index 901c18dc7..c6113ee60 100644
--- a/src/ReferenceIntegrity/ReferenceIntegrityListener.php
+++ b/src/ReferenceIntegrity/ReferenceIntegrityListener.php
@@ -86,15 +86,15 @@ public function preRemove(EventArgs $args)
                             throw new InvalidMappingException(sprintf("Reference '%s' on '%s' should have 'mappedBy' option defined", $property, $meta->getName()));
                         }
 
-                        assert(class_exists($fieldMapping['targetDocument']));
+                        assert(class_exists($fieldMapping->targetDocument ?? $fieldMapping['targetDocument']));
 
-                        $subMeta = $om->getClassMetadata($fieldMapping['targetDocument']);
+                        $subMeta = $om->getClassMetadata($fieldMapping->targetDocument ?? $fieldMapping['targetDocument']);
 
-                        if (!$subMeta->hasField($fieldMapping['mappedBy'])) {
-                            throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $fieldMapping['mappedBy'], $fieldMapping['targetDocument']));
+                        if (!$subMeta->hasField($fieldMapping->mappedBy ?? $fieldMapping['mappedBy'])) {
+                            throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $fieldMapping->mappedBy ?? $fieldMapping['mappedBy'], $fieldMapping->targetDocument ?? $fieldMapping['targetDocument']));
                         }
 
-                        $refReflProp = $subMeta->getReflectionProperty($fieldMapping['mappedBy']);
+                        $refReflProp = $subMeta->getReflectionProperty($fieldMapping->mappedBy ?? $fieldMapping['mappedBy']);
 
                         if ($meta->isCollectionValuedReference($property)) {
                             foreach ($refDoc as $refObj) {
@@ -112,19 +112,19 @@ public function preRemove(EventArgs $args)
                             throw new InvalidMappingException(sprintf("Reference '%s' on '%s' should have 'mappedBy' option defined", $property, $meta->getName()));
                         }
 
-                        assert(class_exists($fieldMapping['targetDocument']));
+                        assert(class_exists($fieldMapping->targetDocument ?? $fieldMapping['targetDocument']));
 
-                        $subMeta = $om->getClassMetadata($fieldMapping['targetDocument']);
+                        $subMeta = $om->getClassMetadata($fieldMapping->targetDocument ?? $fieldMapping['targetDocument']);
 
-                        if (!$subMeta->hasField($fieldMapping['mappedBy'])) {
-                            throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $fieldMapping['mappedBy'], $fieldMapping['targetDocument']));
+                        if (!$subMeta->hasField($fieldMapping->mappedBy ?? $fieldMapping['mappedBy'])) {
+                            throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $fieldMapping->mappedBy ?? $fieldMapping['mappedBy'], $fieldMapping->targetDocument ?? $fieldMapping['targetDocument']));
                         }
 
-                        if (!$subMeta->isCollectionValuedReference($fieldMapping['mappedBy'])) {
-                            throw new InvalidMappingException(sprintf('Reference integrity [%s] mapped property in entity - %s should be a Reference Many', $fieldMapping['mappedBy'], $fieldMapping['targetDocument']));
+                        if (!$subMeta->isCollectionValuedReference($fieldMapping->mappedBy ?? $fieldMapping['mappedBy'])) {
+                            throw new InvalidMappingException(sprintf('Reference integrity [%s] mapped property in entity - %s should be a Reference Many', $fieldMapping->mappedBy ?? $fieldMapping['mappedBy'], $fieldMapping->targetDocument ?? $fieldMapping['targetDocument']));
                         }
 
-                        $refReflProp = $subMeta->getReflectionProperty($fieldMapping['mappedBy']);
+                        $refReflProp = $subMeta->getReflectionProperty($fieldMapping->mappedBy ?? $fieldMapping['mappedBy']);
 
                         if ($meta->isCollectionValuedReference($property)) {
                             foreach ($refDoc as $refObj) {
@@ -143,10 +143,10 @@ public function preRemove(EventArgs $args)
                         break;
                     case Validator::RESTRICT:
                         if ($meta->isCollectionValuedReference($property) && $refDoc->count() > 0) {
-                            throw new ReferenceIntegrityStrictException(sprintf("The reference integrity for the '%s' collection is restricted", $fieldMapping['targetDocument']));
+                            throw new ReferenceIntegrityStrictException(sprintf("The reference integrity for the '%s' collection is restricted", $fieldMapping->targetDocument ?? $fieldMapping['targetDocument']));
                         }
                         if ($meta->isSingleValuedReference($property) && null !== $refDoc) {
-                            throw new ReferenceIntegrityStrictException(sprintf("The reference integrity for the '%s' document is restricted", $fieldMapping['targetDocument']));
+                            throw new ReferenceIntegrityStrictException(sprintf("The reference integrity for the '%s' document is restricted", $fieldMapping->targetDocument ?? $fieldMapping['targetDocument']));
                         }
 
                         break;
diff --git a/src/Sluggable/Mapping/Driver/Xml.php b/src/Sluggable/Mapping/Driver/Xml.php
index a54976b85..89f8d454d 100644
--- a/src/Sluggable/Mapping/Driver/Xml.php
+++ b/src/Sluggable/Mapping/Driver/Xml.php
@@ -76,7 +76,7 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true);
     }
 
     /**
diff --git a/src/Sluggable/Mapping/Driver/Yaml.php b/src/Sluggable/Mapping/Driver/Yaml.php
index eddc7c9c5..167258a22 100644
--- a/src/Sluggable/Mapping/Driver/Yaml.php
+++ b/src/Sluggable/Mapping/Driver/Yaml.php
@@ -85,7 +85,7 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true);
     }
 
     /**
diff --git a/src/Sluggable/SluggableListener.php b/src/Sluggable/SluggableListener.php
index 2b519fe95..485804505 100644
--- a/src/Sluggable/SluggableListener.php
+++ b/src/Sluggable/SluggableListener.php
@@ -440,11 +440,12 @@ private function generateSlug(SluggableAdapter $ea, object $object): void
                 }
 
                 // cut slug if exceeded in length
-                if (isset($mapping['length']) && strlen($slug) > $mapping['length']) {
-                    $slug = substr($slug, 0, $mapping['length']);
+                $length = $mapping->length ?? $mapping['length'] ?? null;
+                if (null !== $length && strlen($slug) > $length) {
+                    $slug = substr($slug, 0, $length);
                 }
 
-                if (isset($mapping['nullable']) && $mapping['nullable'] && 0 === strlen($slug)) {
+                if (($mapping->nullable ?? $mapping['nullable'] ?? false) && 0 === strlen($slug)) {
                     $slug = null;
                 }
 
@@ -546,11 +547,12 @@ private function makeUniqueSlug(SluggableAdapter $ea, object $object, string $pr
             }
 
             $mapping = $meta->getFieldMapping($config['slug']);
-            if (isset($mapping['length']) && strlen($generatedSlug) > $mapping['length']) {
+            $length = $mapping->length ?? $mapping['length'] ?? null;
+            if (null !== $length && strlen($generatedSlug) > $length) {
                 $generatedSlug = substr(
                     $generatedSlug,
                     0,
-                    $mapping['length'] - (strlen($uniqueSuffix) + strlen($config['separator']))
+                    $length - (strlen($uniqueSuffix) + strlen($config['separator']))
                 );
                 $this->exponent = strlen($uniqueSuffix) - 1;
                 if (substr($generatedSlug, -strlen($config['separator'])) == $config['separator']) {
diff --git a/src/SoftDeleteable/Mapping/Validator.php b/src/SoftDeleteable/Mapping/Validator.php
index 0f138e250..2e9d52bd3 100644
--- a/src/SoftDeleteable/Mapping/Validator.php
+++ b/src/SoftDeleteable/Mapping/Validator.php
@@ -52,8 +52,8 @@ public static function validateField(ClassMetadata $meta, $field)
 
         $fieldMapping = $meta->getFieldMapping($field);
 
-        if (!in_array($fieldMapping['type'], self::$validTypes, true)) {
-            throw new InvalidMappingException(sprintf('Field "%s" (type "%s") must be of one of the following types: "%s" in entity %s', $field, $fieldMapping['type'], implode(', ', self::$validTypes), $meta->getName()));
+        if (!in_array($fieldMapping->type ?? $fieldMapping['type'], self::$validTypes, true)) {
+            throw new InvalidMappingException(sprintf('Field "%s" (type "%s") must be of one of the following types: "%s" in entity %s', $field, $fieldMapping->type ?? $fieldMapping['type'], implode(', ', self::$validTypes), $meta->getName()));
         }
     }
 }
diff --git a/src/Sortable/Mapping/Driver/Xml.php b/src/Sortable/Mapping/Driver/Xml.php
index 658e692c3..e02b81ca1 100644
--- a/src/Sortable/Mapping/Driver/Xml.php
+++ b/src/Sortable/Mapping/Driver/Xml.php
@@ -90,7 +90,7 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true);
     }
 
     /**
diff --git a/src/Sortable/Mapping/Driver/Yaml.php b/src/Sortable/Mapping/Driver/Yaml.php
index d09d815ab..500a4e025 100644
--- a/src/Sortable/Mapping/Driver/Yaml.php
+++ b/src/Sortable/Mapping/Driver/Yaml.php
@@ -97,7 +97,7 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true);
     }
 
     /**
diff --git a/src/Timestampable/Mapping/Driver/Xml.php b/src/Timestampable/Mapping/Driver/Xml.php
index e94415ab4..e21716806 100644
--- a/src/Timestampable/Mapping/Driver/Xml.php
+++ b/src/Timestampable/Mapping/Driver/Xml.php
@@ -105,6 +105,6 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true);
     }
 }
diff --git a/src/Timestampable/Mapping/Driver/Yaml.php b/src/Timestampable/Mapping/Driver/Yaml.php
index 3c1600f73..0dee60da7 100644
--- a/src/Timestampable/Mapping/Driver/Yaml.php
+++ b/src/Timestampable/Mapping/Driver/Yaml.php
@@ -109,6 +109,6 @@ protected function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true);
     }
 }
diff --git a/src/Translatable/Query/TreeWalker/TranslationWalker.php b/src/Translatable/Query/TreeWalker/TranslationWalker.php
index 4801ad4d1..7fad4792b 100644
--- a/src/Translatable/Query/TreeWalker/TranslationWalker.php
+++ b/src/Translatable/Query/TreeWalker/TranslationWalker.php
@@ -297,7 +297,7 @@ private function prepareTranslatedComponents(): void
 
                     $mappingFK = $transMeta->getFieldMapping('foreignKey');
                     $mappingPK = $meta->getFieldMapping($identifier);
-                    $fkColName = $this->getCastedForeignKey($compTblAlias.'.'.$idColName, $mappingFK['type'], $mappingPK['type']);
+                    $fkColName = $this->getCastedForeignKey($compTblAlias.'.'.$idColName, $mappingFK->type ?? $mappingFK['type'], $mappingPK->type ?? $mappingPK['type']);
                     $sql .= ' AND '.$tblAlias.'.'.$quoteStrategy->getColumnName('foreignKey', $transMeta, $this->platform)
                         .' = '.$fkColName;
                 }
@@ -309,10 +309,10 @@ private function prepareTranslatedComponents(): void
                 // Treat translation as original field type
                 $fieldMapping = $meta->getFieldMapping($field);
                 if ((($this->platform instanceof AbstractMySQLPlatform)
-                    && in_array($fieldMapping['type'], ['decimal'], true))
+                    && in_array($fieldMapping->type ?? $fieldMapping['type'], ['decimal'], true))
                     || (!($this->platform instanceof AbstractMySQLPlatform)
-                    && !in_array($fieldMapping['type'], ['datetime', 'datetimetz', 'date', 'time'], true))) {
-                    $type = Type::getType($fieldMapping['type']);
+                    && !in_array($fieldMapping->type ?? $fieldMapping['type'], ['datetime', 'datetimetz', 'date', 'time'], true))) {
+                    $type = Type::getType($fieldMapping->type ?? $fieldMapping['type']);
 
                     // In ORM 2.x, $fieldMapping is an array. In ORM 3.x, it's a data object. Always cast to an array for compatibility across versions.
                     $substituteField = 'CAST('.$substituteField.' AS '.$type->getSQLDeclaration((array) $fieldMapping, $this->platform).')';
diff --git a/src/Tree/Mapping/Validator.php b/src/Tree/Mapping/Validator.php
index f2d0fcd9f..8c9258eed 100644
--- a/src/Tree/Mapping/Validator.php
+++ b/src/Tree/Mapping/Validator.php
@@ -9,6 +9,7 @@
 
 namespace Gedmo\Tree\Mapping;
 
+use Doctrine\ORM\Mapping\FieldMapping;
 use Doctrine\Persistence\Mapping\ClassMetadata;
 use Gedmo\Exception\InvalidMappingException;
 
@@ -97,7 +98,7 @@ public function isValidField($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], self::VALID_TYPES, true);
+        return $mapping && in_array($this->getMappingType($mapping), self::VALID_TYPES, true);
     }
 
     /**
@@ -112,7 +113,7 @@ public function isValidFieldForPath($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], $this->validPathTypes, true);
+        return $mapping && in_array($this->getMappingType($mapping), $this->validPathTypes, true);
     }
 
     /**
@@ -127,7 +128,7 @@ public function isValidFieldForPathSource($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], $this->validPathSourceTypes, true);
+        return $mapping && in_array($this->getMappingType($mapping), $this->validPathSourceTypes, true);
     }
 
     /**
@@ -142,7 +143,7 @@ public function isValidFieldForPathHash($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], $this->validPathHashTypes, true);
+        return $mapping && in_array($this->getMappingType($mapping), $this->validPathHashTypes, true);
     }
 
     /**
@@ -157,7 +158,7 @@ public function isValidFieldForLockTime($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && ('date' === $mapping['type'] || 'datetime' === $mapping['type'] || 'timestamp' === $mapping['type']);
+        return $mapping && ('date' === $this->getMappingType($mapping) || 'datetime' === $this->getMappingType($mapping) || 'timestamp' === $this->getMappingType($mapping));
     }
 
     /**
@@ -172,7 +173,7 @@ public function isValidFieldForRoot($meta, $field)
     {
         $mapping = $meta->getFieldMapping($field);
 
-        return $mapping && in_array($mapping['type'], $this->validRootTypes, true);
+        return $mapping && in_array($this->getMappingType($mapping), $this->validRootTypes, true);
     }
 
     /**
@@ -252,4 +253,16 @@ public function validateMaterializedPathTreeMetadata($meta, array $config)
             throw new InvalidMappingException('Missing properties: '.implode(', ', $missingFields)." in class - {$meta->getName()}");
         }
     }
+
+    /**
+     * @param FieldMapping|array<string, scalar> $mapping
+     */
+    private function getMappingType($mapping): string
+    {
+        if ($mapping instanceof FieldMapping) {
+            return $mapping->type;
+        }
+
+        return $mapping['type'];
+    }
 }
diff --git a/src/Tree/Strategy/AbstractMaterializedPath.php b/src/Tree/Strategy/AbstractMaterializedPath.php
index 0f4cc8372..c1eef0df6 100644
--- a/src/Tree/Strategy/AbstractMaterializedPath.php
+++ b/src/Tree/Strategy/AbstractMaterializedPath.php
@@ -225,7 +225,7 @@ public function updateNode(ObjectManager $om, $node, AdapterInterface $ea)
         // default behavior: if PathSource field is a string, we append the ID to the path
         // path_append_id is true: always append id
         // path_append_id is false: never append id
-        if (true === $config['path_append_id'] || ('string' === $fieldMapping['type'] && false !== $config['path_append_id'])) {
+        if (true === $config['path_append_id'] || ('string' === ($fieldMapping->type ?? $fieldMapping['type']) && false !== $config['path_append_id'])) {
             if (method_exists($meta, 'getIdentifierValue')) {
                 $identifier = $meta->getIdentifierValue($node);
             } else {
diff --git a/src/Tree/Strategy/ORM/Closure.php b/src/Tree/Strategy/ORM/Closure.php
index 3e68f6b3d..9d80e51a4 100644
--- a/src/Tree/Strategy/ORM/Closure.php
+++ b/src/Tree/Strategy/ORM/Closure.php
@@ -524,7 +524,9 @@ protected function setLevelFieldOnPendingNodes(ObjectManager $em)
             }
 
             // Avoid type conversion performance penalty
-            $type = 'integer' === $mapping['type'] ? ArrayParameterType::INTEGER : ArrayParameterType::STRING;
+            $type = 'integer' === ($mapping->type ?? $mapping['type'])
+                ? ArrayParameterType::INTEGER
+                : ArrayParameterType::STRING;
 
             // We calculate levels for all nodes
             $sql = 'SELECT c.descendant, MAX(c.depth) + 1 AS levelNum ';
diff --git a/src/Uploadable/Mapping/Validator.php b/src/Uploadable/Mapping/Validator.php
index 4dfce4c1b..73571800b 100644
--- a/src/Uploadable/Mapping/Validator.php
+++ b/src/Uploadable/Mapping/Validator.php
@@ -150,7 +150,7 @@ public static function validateField($meta, $field, $uploadableField, $validFiel
 
         $fieldMapping = $meta->getFieldMapping($field);
 
-        if (!in_array($fieldMapping['type'], $validFieldTypes, true)) {
+        if (!in_array($fieldMapping->type ?? $fieldMapping['type'], $validFieldTypes, true)) {
             $msg = 'Field "%s" to work as an "%s" field must be of one of the following types: "%s".';
 
             throw new InvalidMappingException(sprintf($msg, $field, $uploadableField, implode(', ', $validFieldTypes)));


Test patch:
diff --git a/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Driver/Attribute.php b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Driver/Attribute.php
index eeaf0619a..2794af893 100644
--- a/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Driver/Attribute.php
+++ b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Driver/Attribute.php
@@ -49,7 +49,7 @@ public function readExtendedMetadata($meta, array &$config)
                 // validate encoding type
                 $mapping = $meta->getFieldMapping($field);
 
-                if ('string' !== $mapping['type']) {
+                if ('string' !== ($mapping->type ?? $mapping['type'])) {
                     throw new \Exception('Only strings can be encoded');
                 }