From 53d9fc22d091bd76cc7f00e5427d5742e0198d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 15 Oct 2025 13:20:25 +0200 Subject: [PATCH 1/4] Add getFieldValue and setFieldValue to ClassMetadata interface --- UPGRADE.md | 10 ++++++++++ src/Persistence/Mapping/ClassMetadata.php | 2 ++ 2 files changed, 12 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 5bf1ef0a..d7a9a869 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -6,6 +6,16 @@ awareness about deprecated code. - Use of our low-overhead runtime deprecation API, details: https://github.com/doctrine/deprecations/ +# Upgrade to 4.2 + +## Add `getFieldValue` and `setFieldValue` to `ClassMetadata` implementation + +The interface `Doctrine\Persistence\Mapping\ClassMetadata` has two new methods: +- `getFieldValue(object $object, string $field)` +- `setFieldValue(object $object, string $field, mixed $value): void` + +Not implementing these methods is deprecated. They will be required in 5.0. + # Upgrade to 4.0 ## BC Break: Removed `StaticReflectionService` diff --git a/src/Persistence/Mapping/ClassMetadata.php b/src/Persistence/Mapping/ClassMetadata.php index 1d0345c3..3ca89075 100644 --- a/src/Persistence/Mapping/ClassMetadata.php +++ b/src/Persistence/Mapping/ClassMetadata.php @@ -10,6 +10,8 @@ * Contract for a Doctrine persistence layer ClassMetadata class to implement. * * @template-covariant T of object + * @method mixed getFieldValue(object $entity, string $field) + * @method void setFieldValue(object $entity, string $field, mixed $value) */ interface ClassMetadata { From 3e51fb4ef12d4bac1da3221d0c1017c8126d019d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 15 Oct 2025 12:07:00 +0200 Subject: [PATCH 2/4] Support closure in PHPDriver Add test on unbound scope of the included file Deprecate updating $metadata variable in the php file directly Exclude _files directory from PHPStan inspection --- UPGRADE.md | 25 ++++++++ composer.json | 1 + docs/en/reference/index.rst | 11 +++- phpstan.neon | 2 +- src/Persistence/Mapping/Driver/PHPDriver.php | 31 ++++++++++ tests/Persistence/Mapping/PHPDriverTest.php | 62 ++++++++++++++++++- ...ersistence.Mapping.PHPTestEntityAssert.php | 8 +++ ...rsistence.Mapping.PHPTestEntityClosure.php | 9 +++ ...ence.Mapping.PHPTestIncorrectUseStatic.php | 9 +++ ...stence.Mapping.PHPTestIncorrectUseThis.php | 7 +++ 10 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestEntityAssert.php create mode 100644 tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestEntityClosure.php create mode 100644 tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestIncorrectUseStatic.php create mode 100644 tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestIncorrectUseThis.php diff --git a/UPGRADE.md b/UPGRADE.md index d7a9a869..ca89d1bc 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -16,6 +16,31 @@ The interface `Doctrine\Persistence\Mapping\ClassMetadata` has two new methods: Not implementing these methods is deprecated. They will be required in 5.0. +## Deprecated modifying `$metadata` in PHP mapping files + +Relying on the `$metadata` variable directly in PHP mapping files is deprecated. +Instead, wrap the code in a closure that is returned by the configuration file. + +Before: + +```php +name = \App\Entity\User::class; +``` + +After: + +```php +name = \App\Entity\User::class; +}; +``` + # Upgrade to 4.0 ## BC Break: Removed `StaticReflectionService` diff --git a/composer.json b/composer.json index 6e5a5ccf..e407cc6b 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ ], "require": { "php": "^8.1", + "doctrine/deprecations": "^1", "doctrine/event-manager": "^1 || ^2", "psr/cache": "^1.0 || ^2.0 || ^3.0" }, diff --git a/docs/en/reference/index.rst b/docs/en/reference/index.rst index df529bf5..e17eca8f 100644 --- a/docs/en/reference/index.rst +++ b/docs/en/reference/index.rst @@ -258,10 +258,15 @@ mapping metadata. .. code-block:: php use App\Model\User; + use Doctrine\Persistence\Mapping\ClassMetadata; + + return function (ClassMetadata $metadata): void { + $metadata->name = User::class; + + // ... - $metadata->name = User::class; + }; - // ... StaticPHPDriver -------------- @@ -284,6 +289,8 @@ Your class in ``App\Model\User`` would look like the following. namespace App\Model; + use Doctrine\Persistence\Mapping\ClassMetadata; + final class User { // ... diff --git a/phpstan.neon b/phpstan.neon index 2d2827e9..dbf7d337 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,7 +14,7 @@ parameters: - tests/Persistence/Mapping/_files/colocated/Foo.mphp excludePaths: - - tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestEntity.php + - tests/Persistence/Mapping/_files/Doctrine.*.php ignoreErrors: - '#Variable property access on \$this\(Doctrine\\Persistence\\Reflection\\TypedNoDefaultReflectionProperty\)\.#' diff --git a/src/Persistence/Mapping/Driver/PHPDriver.php b/src/Persistence/Mapping/Driver/PHPDriver.php index 10061b82..318c7fa6 100644 --- a/src/Persistence/Mapping/Driver/PHPDriver.php +++ b/src/Persistence/Mapping/Driver/PHPDriver.php @@ -4,7 +4,11 @@ namespace Doctrine\Persistence\Mapping\Driver; +use Closure; +use CompileError; +use Doctrine\Deprecations\Deprecation; use Doctrine\Persistence\Mapping\ClassMetadata; +use Error; /** * The PHPDriver includes php files which just populate ClassMetadataInfo @@ -35,6 +39,33 @@ public function loadMetadataForClass(string $className, ClassMetadata $metadata) */ protected function loadMappingFile(string $file): array { + try { + $callback = Closure::bind(static function (string $file): mixed { + $metadata = null; + + return include $file; + }, null, null)($file); + } catch (CompileError $e) { + throw $e; + } catch (Error) { + // Calling any method on $metadata=null will raise an Error + // Falling back to legacy behavior of expecting $metadata to be populated + $callback = null; + } + + if ($callback instanceof Closure) { + $callback($this->metadata); + + return [$this->metadata->getName() => $this->metadata]; + } + + Deprecation::trigger( + 'doctrine/persistence', + 'https://github.com/doctrine/persistence/pull/450', + 'Not returning a Closure from a PHP mapping file is deprecated', + ); + + unset($callback); $metadata = $this->metadata; include $file; diff --git a/tests/Persistence/Mapping/PHPDriverTest.php b/tests/Persistence/Mapping/PHPDriverTest.php index 069421a5..429a2f1b 100644 --- a/tests/Persistence/Mapping/PHPDriverTest.php +++ b/tests/Persistence/Mapping/PHPDriverTest.php @@ -4,22 +4,80 @@ namespace Doctrine\Tests\Persistence\Mapping; +use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\Driver\PHPDriver; use Doctrine\Tests\DoctrineTestCase; +use Error; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use PHPUnit\Framework\Attributes\TestWith; class PHPDriverTest extends DoctrineTestCase { - public function testLoadMetadata(): void + use VerifyDeprecations; + + /** @phpstan-param class-string $className */ + #[IgnoreDeprecations] + #[TestWith([PHPTestEntity::class])] + #[TestWith([PHPTestEntityAssert::class])] + public function testLoadMetadata(string $className): void + { + $metadata = $this->createMock(ClassMetadata::class); + $metadata->expects(self::once())->method('getFieldNames'); + $driver = new PHPDriver([__DIR__ . '/_files']); + + $this->expectDeprecationWithIdentifier('https://github.com/doctrine/persistence/pull/450'); + $driver->loadMetadataForClass($className, $metadata); + } + + public function testLoadMetadataWithClosure(): void { $metadata = $this->createMock(ClassMetadata::class); $metadata->expects(self::once())->method('getFieldNames'); $driver = new PHPDriver([__DIR__ . '/_files']); - $driver->loadMetadataForClass(PHPTestEntity::class, $metadata); + $driver->loadMetadataForClass(PHPTestEntityClosure::class, $metadata); + } + + public function testLoadMetadataClosureNotBoundToObject(): void + { + $metadata = $this->createMock(ClassMetadata::class); + $driver = new PHPDriver([__DIR__ . '/_files']); + + $this->expectException(Error::class); + $this->expectExceptionMessage('Using $this when not in object context'); + + $driver->loadMetadataForClass(PHPTestIncorrectUseThis::class, $metadata); + } + + public function testLoadMetadataClosureNotBoundToClass(): void + { + $metadata = $this->createMock(ClassMetadata::class); + $driver = new PHPDriver([__DIR__ . '/_files']); + + $this->expectException(Error::class); + $this->expectExceptionMessage('Cannot use "static" in the global scope'); + + $driver->loadMetadataForClass(PHPTestIncorrectUseStatic::class, $metadata); } } class PHPTestEntity { } + +class PHPTestEntityAssert +{ +} + +class PHPTestEntityClosure +{ +} + +class PHPTestIncorrectUseThis +{ +} + +class PHPTestIncorrectUseStatic +{ +} diff --git a/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestEntityAssert.php b/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestEntityAssert.php new file mode 100644 index 00000000..7ec5a964 --- /dev/null +++ b/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestEntityAssert.php @@ -0,0 +1,8 @@ +getFieldNames(); diff --git a/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestEntityClosure.php b/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestEntityClosure.php new file mode 100644 index 00000000..2cefa33a --- /dev/null +++ b/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestEntityClosure.php @@ -0,0 +1,9 @@ +getFieldNames(); +}; diff --git a/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestIncorrectUseStatic.php b/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestIncorrectUseStatic.php new file mode 100644 index 00000000..dd38b03a --- /dev/null +++ b/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestIncorrectUseStatic.php @@ -0,0 +1,9 @@ +isIdentifier(static::class); +}; diff --git a/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestIncorrectUseThis.php b/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestIncorrectUseThis.php new file mode 100644 index 00000000..3373a96d --- /dev/null +++ b/tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestIncorrectUseThis.php @@ -0,0 +1,7 @@ +setGlobalBasename('global-mapping'); +}; From acde25ea5d4442797344d5fa6a5b0e52f7979cec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 20:08:31 +0000 Subject: [PATCH 3/4] Bump doctrine/.github from 12.0.0 to 12.1.0 Bumps [doctrine/.github](https://github.com/doctrine/.github) from 12.0.0 to 12.1.0. - [Release notes](https://github.com/doctrine/.github/releases) - [Commits](https://github.com/doctrine/.github/compare/12.0.0...12.1.0) --- updated-dependencies: - dependency-name: doctrine/.github dependency-version: 12.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/coding-standards.yml | 2 +- .github/workflows/continuous-integration.yml | 2 +- .github/workflows/release-on-milestone-closed.yml | 2 +- .github/workflows/static-analysis.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index f115b910..0ad9b907 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -12,6 +12,6 @@ on: jobs: coding-standards: name: "Coding Standards" - uses: "doctrine/.github/.github/workflows/coding-standards.yml@12.0.0" + uses: "doctrine/.github/.github/workflows/coding-standards.yml@12.1.0" with: composer-root-version: "3.0" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ebfb6df3..1db68268 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -12,7 +12,7 @@ on: jobs: phpunit: name: "PHPUnit" - uses: "doctrine/.github/.github/workflows/continuous-integration.yml@12.0.0" + uses: "doctrine/.github/.github/workflows/continuous-integration.yml@12.1.0" with: composer-root-version: "3.0" php-versions: '["7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4", "8.5"]' diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index 329e5571..89dba39e 100644 --- a/.github/workflows/release-on-milestone-closed.yml +++ b/.github/workflows/release-on-milestone-closed.yml @@ -8,7 +8,7 @@ on: jobs: release: name: "Git tag, release & create merge-up PR" - uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@12.0.0" + uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@12.1.0" secrets: GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 7974f99d..0f0fa07e 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -12,6 +12,6 @@ on: jobs: static-analysis: name: "Static Analysis" - uses: "doctrine/.github/.github/workflows/phpstan.yml@12.0.0" + uses: "doctrine/.github/.github/workflows/phpstan.yml@12.1.0" with: composer-root-version: "3.0" From 4431dcf375ca7e6a24ee4a761ad8f1a4072d71ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Thu, 30 Oct 2025 08:34:44 +0100 Subject: [PATCH 4/4] Setup documentation workflow --- .github/workflows/documentation.yml | 20 ++++++++++++++++++++ composer.json | 3 +++ docs/.gitignore | 3 +++ docs/composer.json | 5 +++++ docs/en/sidebar.rst | 2 ++ 5 files changed, 33 insertions(+) create mode 100644 .github/workflows/documentation.yml create mode 100644 docs/.gitignore create mode 100644 docs/composer.json diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000..90c54d67 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,20 @@ +name: "Documentation" + +on: + pull_request: + branches: + - "*.x" + paths: + - .github/workflows/documentation.yml + - docs/** + push: + branches: + - "*.x" + paths: + - .github/workflows/documentation.yml + - docs/** + +jobs: + documentation: + name: "Documentation" + uses: "doctrine/.github/.github/workflows/documentation.yml@12.2.0" diff --git a/composer.json b/composer.json index ca364c9d..207ecfd6 100644 --- a/composer.json +++ b/composer.json @@ -53,5 +53,8 @@ "dealerdirect/phpcodesniffer-composer-installer": true, "composer/package-versions-deprecated": true } + }, + "scripts": { + "docs": "composer --working-dir docs update && ./docs/vendor/bin/build-docs.sh @additional_args" } } diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..f26c03fc --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +/composer.lock +/vendor/ +/output/ diff --git a/docs/composer.json b/docs/composer.json new file mode 100644 index 00000000..c5a4e144 --- /dev/null +++ b/docs/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "doctrine/docs-builder": "^1.0" + } +} diff --git a/docs/en/sidebar.rst b/docs/en/sidebar.rst index f89f2bba..f18d38f4 100644 --- a/docs/en/sidebar.rst +++ b/docs/en/sidebar.rst @@ -1,3 +1,5 @@ +:orphan: + .. toctree:: :depth: 3