Compare commits

...

119 Commits
3.3.3 ... 3.4.1

Author SHA1 Message Date
Grégoire Paris
92e2f6db83 Merge pull request #12012 from greg0ire/revert-allfields-dto
Revert "add capability to use allfields sql notation"
2025-06-21 12:44:26 +02:00
Grégoire Paris
aa624f64c1 Remove trailing whitespace 2025-06-21 11:58:51 +02:00
Grégoire Paris
e1675eb371 Revert "add capability to use allfields sql notation"
This reverts commit 12c721f528.

This feature introduces several issues:

- It adds alias.*, which is a first, for instance you cannot do
SELECT u.* FROM User u
- If introduces coupling between property order in mapping fields and
  the result.
2025-06-21 11:58:42 +02:00
Grégoire Paris
cc2b6385a1 Merge pull request #12011 from greg0ire/3.4.x
Merge 2.20.x up into 3.4.x
2025-06-21 11:48:10 +02:00
Grégoire Paris
a64bed9bbb Merge remote-tracking branch 'origin/2.20.x' into 3.4.x 2025-06-21 11:11:52 +02:00
Grégoire Paris
3272e1c0af Merge pull request #12008 from greg0ire/add-test-to-todo-list
Ensure proxies implementations behave the same on entity not found
2025-06-20 00:24:56 +02:00
Grégoire Paris
69da22d517 Ensure proxies implementations behave the same on entity not found
Both implementations are supposed to throw EntityNotFoundException
2025-06-19 10:07:06 +02:00
Grégoire Paris
06109f360f Merge pull request #12002 from greg0ire/relax-type-declarations
Make proxyDir and proxyNs nullable and optional
2025-06-19 08:00:18 +02:00
Grégoire Paris
06a9ef1127 Make proxyDir and proxyNs nullable and optional
When using native lazy objects, it should be possible to omit these
arguments, hence the default value.
Also, when using native lazy objects, one should not have to configure
the corresponding Configuration attributes, which means
EntityManager__construct() should be able to pass null to this class,
hence the nullability.

Fixes #11997
2025-06-18 23:23:30 +02:00
Alexander M. Turek
5d21bb158b Fix calls to Application::add() (#12006) 2025-06-18 08:58:26 +02:00
Grégoire Paris
c74df3fab3 Merge pull request #12001 from greg0ire/lazy-objects-by-default
Enable native lazy objects by default
2025-06-17 23:50:46 +02:00
Grégoire Paris
f2c902ee03 Rewrite test with native lazy ghost
I do not think this needs to be tested on all versions of PHP, using
native lazy objects allows us to remove a deprecation.
2025-06-17 23:35:47 +02:00
Grégoire Paris
4e5e3c5e50 Enable native lazy objects by default
This should make the test suite look less like a christmas tree.
2025-06-17 23:09:48 +02:00
Grégoire Paris
da697f218f Merge pull request #12000 from greg0ire/fix-var-name
Use the correct environment variable name for lazy objects and enable them by default
2025-06-17 21:15:38 +02:00
Grégoire Paris
4f47a80deb Use the correct environment variable name for lazy objects
The test suite checks for ENABLE_NATIVE_LAZY_OBJECTS
I have also renamed the matrix variable for the sake of consistency.
2025-06-17 08:35:16 +02:00
Grégoire Paris
ab89517093 Merge pull request #11987 from greg0ire/update-branch-metadata
Update branch metadata
2025-06-16 08:37:20 +02:00
Grégoire Paris
48a51d8470 Merge pull request #11992 from eltharin/error_doc_codeblock
repair code block bad showing
2025-06-16 08:36:13 +02:00
eltharin
ab11244f08 repair code block bad showing 2025-06-16 08:03:47 +02:00
Grégoire Paris
a1c2be140d Update branch metadata
- 3.5.x has been created
- 3.4.0 has been released
- 3.3.x is no longer maintained
2025-06-14 13:50:55 +02:00
Grégoire Paris
4664373bd0 Merge pull request #11985 from doctrine/3.3.x-merge-up-into-3.4.x_KXrSCX8l
Merge release 3.3.4 into 3.4.x
2025-06-14 13:47:14 +02:00
Grégoire Paris
5ab6b74f14 Merge pull request #11984 from doctrine/2.20.x
Merge 2.20.x up into 3.3.x
2025-06-14 13:32:43 +02:00
Grégoire Paris
71550106d4 Merge pull request #11975 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github-7.3.0
Bump doctrine/.github from 7.2.2 to 7.3.0
2025-06-09 22:24:12 +02:00
dependabot[bot]
36011f0d0f Bump doctrine/.github from 7.2.2 to 7.3.0
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 7.2.2 to 7.3.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/7.2.2...7.3.0)

---
updated-dependencies:
- dependency-name: doctrine/.github
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 07:13:58 +00:00
Grégoire Paris
c97d775370 Merge pull request #11963 from dbu/update-doc-building
cleanup doc building instructions
2025-06-09 07:45:09 +02:00
Grégoire Paris
97b29bb063 Merge pull request #11973 from eltharin/add_constructor
Add constructor argument
2025-06-07 09:41:24 +02:00
Grégoire Paris
e9f0345a97 Merge pull request #11966 from greg0ire/partial-revert-10162-2
Partially revert to stdout
2025-06-07 09:41:03 +02:00
eltharin
b7fff508a4 add argument in constructor 2025-06-06 18:39:24 +02:00
Grégoire Paris
c6fa14ed52 Merge pull request #11971 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2025-06-06 13:05:27 +02:00
Grégoire Paris
05c8c5f114 Merge pull request #11969 from greg0ire/3.3.x
Merge 2.20.x up into 3.3.x
2025-06-06 12:29:34 +02:00
Gregoire PARIS
1dbdb0e895 Merge remote-tracking branch 'origin/2.20.x' into 3.3.x 2025-06-06 12:08:24 +02:00
Grégoire Paris
0feb09d0d6 Partially revert to stdout
This command's purpose is to provide structured data, except for a call
to caution() that warns the user in case they do not have any mapped
entities or they have errors.
2025-06-06 07:57:38 +02:00
Grégoire Paris
fe5f8bbaa1 Merge pull request #11965 from greg0ire/partial-revert-10162
Revert to stdout for MappingDescribeCommand
2025-06-06 00:04:07 +02:00
Grégoire Paris
ecf3cec376 chore: ignore deprecations from Symfony
Symfony 7.3 is not available to all of our users, so we cannot switch to
native lazy objects, which require a PHP version higher than the lowest
PHP version we support.
2025-06-05 23:19:32 +02:00
Grégoire Paris
0a714db4d9 Revert to stdout for MappingDescribeCommand
In f256d996cc, I did a global move to
stderr for notifications, and went a bit overboard for
MappingDescribeCommand, which purpose is to output a description.
2025-06-05 23:07:28 +02:00
David Buchmann
471fda8d0b cleanup doc building instructions 2025-06-05 07:44:37 +02:00
Grégoire Paris
dfe32c2f74 Unwrap literalinclude block (#11962)
For some reason, it does not appear to work when nested inside a
code-block directive. Anyway, if you specify the language attribute, you
get markup identical to what you obtain when using code-block and
literalinclude, so this wrapping seems unneeded.
2025-06-04 00:18:59 +02:00
Grégoire Paris
c51ba3ce6b Merge pull request #11951 from dbu/fix-doc-syntax
insert blank line before code in code-block
2025-05-27 14:08:44 +02:00
David Buchmann
fe025e8d23 insert blank line before code in code-block 2025-05-27 08:59:14 +02:00
Grégoire Paris
0a43e4af8f Merge pull request #11946 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2025-05-25 18:35:30 +02:00
Grégoire Paris
7111cc09f3 Merge pull request #11945 from greg0ire/3.3.x
Merge 2.20.x up into 3.3.x
2025-05-25 18:12:06 +02:00
Grégoire Paris
777504b9c4 Merge remote-tracking branch 'origin/2.20.x' into 3.3.x 2025-05-24 19:19:52 +02:00
Dimitri Dovgan
9d9985076a Add missing closing bracket (#11937)
Corrected a malformed attribute in the documentation.
2025-05-18 12:08:50 +02:00
Olivier Massot
35d301b052 Association Mappings: replace assertions by explicit exceptions (#11896) 2025-05-16 08:37:08 +02:00
Grégoire Paris
083b241c81 Merge pull request #11846 from eltharin/all_fields
add capability to use allfields sql notation
2025-05-08 10:55:51 +02:00
Grégoire Paris
ae2957cf7e Merge pull request #11932 from dbannik/2.20.3-issue-11931
#11931 Bug when change sql filter [Related issue #11694]
2025-05-06 07:53:48 +02:00
Dmitry Bannik
e172b3bf9c #11931 Bug when change sql filter [Related issue #11694]
This fix takes into account the invalidation of the filter sql for SingleTablePersister and JoinedSubclassPersister
2025-05-05 23:43:27 +03:00
Grégoire Paris
c9c6e8da2e Merge pull request #11834 from dbu/document-generated-columns
document how to work with generated columns
2025-05-05 10:05:55 +02:00
Grégoire Paris
b9989555fd Merge pull request #11927 from greg0ire/3.4.x
Merge 3.3.x up into 3.4.x
2025-05-02 20:26:12 +02:00
Grégoire Paris
80a79f6d2d Merge remote-tracking branch 'origin/3.3.x' into 3.4.x 2025-05-02 19:48:18 +02:00
Grégoire Paris
9a3f5579f1 Merge pull request #11921 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2025-04-24 18:59:58 +02:00
eltharin
12c721f528 add capability to use allfields sql notation
in a dto, this PR allow to call u.* to get all fileds fo u entity in one call,
2025-04-22 21:40:54 +02:00
Grégoire Paris
9a9c3e8aba Merge pull request #11847 from eltharin/newentityInDto
add capability to hydrate an entity in a dto
2025-04-22 21:00:16 +02:00
eltharin
46a020108d add capability to hydrate an entity in a dto
this PR allow to hydrate data in an entity  nested in a dto
2025-04-21 14:29:19 +02:00
Grégoire Paris
b286d6cd2c Merge pull request #11902 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2025-04-14 23:38:21 +02:00
Benjamin Eberlei
443cf92242 Merge pull request #11852 from beberlei/PropertyHookSupport
Final tests and adjustments to allow mapping properties with hooks.
Property hooks are not supported when using `symfony/var-exporter`.
2025-04-12 11:33:27 +02:00
Benjamin Eberlei
eb3b984132 Add support for PHP 8.4 Lazy Objects RFC with configuration flag (#11853)
* Introduce PHP 8.4 lazy proxy/ghost API.

* Call setRawValueWithoutLazyInitialization for support with lazy proxy.

* Refactorings

* Revert test change partially and skip with lazy objects.

* Houskeeping: phpcs

* Run with ENABLE_LAZY_PROXY=1 in php 8.4 matrix.

* Fix ci

* Transient properties are not skipping lazy initialization anymore, to expensive and could lead to errors. Adjust lifecycle test that uses transient properittes for assertions.

* Restore behavior preventing property hook use in 8.4 in unsupported coditions

* Add \ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE

Co-authored-by: Nicolas Grekas <nicolas.grekas@gmail.com>

* Rename isNativeLazyObjectsEnabled/enableNativeLazyObjects.

* Housekeeping: phpcs

* Update advanced-configuration docs and make proxy config variables not required anymore with native lazy objects.

* Move code around

* Apply suggestions from code review

Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>

* Pick suggestions

---------

Co-authored-by: Nicolas Grekas <nicolas.grekas@gmail.com>
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2025-03-29 23:14:13 +01:00
Grégoire Paris
04395f98f9 Merge pull request #11887 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2025-03-25 16:25:25 +01:00
Grégoire Paris
0c10010f9f Merge pull request #11884 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2025-03-24 22:34:05 +01:00
Grégoire Paris
be8da83aca Merge pull request #10624 from simPod/deferrable
feat: allow setting foreign key as deferrable
2025-03-23 10:09:10 +01:00
Grégoire Paris
f5ab687226 Merge pull request #11876 from greg0ire/address-reflfield-depr
Address deprecation of ClassMetadata::$reflFields
2025-03-19 15:53:29 +01:00
Grégoire Paris
742eead849 Merge pull request #11878 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2025-03-18 21:38:05 +01:00
Grégoire Paris
f98e871913 Address deprecation of ClassMetadata::$reflFields
We should use the newly introduced ClassMetadata::$propertyAccessors instead.
See https://github.com/doctrine/orm/pull/11659
2025-03-18 19:39:48 +01:00
Grégoire Paris
4b0c11978e Merge pull request #11875 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2025-03-18 14:25:17 +01:00
Grégoire Paris
0ef5610a6c Merge pull request #11873 from beberlei/GH-11659-FollowUp1
Bugfix: Missed a spot using getUnderlyingReflector
2025-03-16 23:18:10 +01:00
Benjamin Eberlei
e29d0e977d Bugfix: Missed a spot using getUnderlyingReflector 2025-03-15 17:39:14 +01:00
Grégoire Paris
d540f73778 Merge pull request #11659 from beberlei/PropertyHooks
Necessary refactorings for Property hooks
2025-02-27 20:04:08 +01:00
Benjamin Eberlei
201d751a26 Allow access to underlying reflector for property accessor. 2025-02-26 00:48:30 +01:00
Benjamin Eberlei
6308b2fd86 Update tests/Tests/ORM/Mapping/PropertyAccessors/ReadOnlyAccessorTest.php
Co-authored-by: Claudio Zizza <859964+SenseException@users.noreply.github.com>
2025-02-26 00:38:41 +01:00
Benjamin Eberlei
8f99e84438 Update src/Mapping/PropertyAccessors/EnumPropertyAccessor.php
Co-authored-by: Claudio Zizza <859964+SenseException@users.noreply.github.com>
2025-02-26 00:34:12 +01:00
Benjamin Eberlei
e36b7755e9 Houskeeping: phpcs 2025-02-23 19:43:26 +01:00
Benjamin Eberlei
7b4d869b31 Merge branch '3.4.x' into PropertyHooks 2025-02-23 19:23:20 +01:00
Grégoire Paris
8873109b4f Merge pull request #11840 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2025-02-18 23:12:40 +01:00
David Buchmann
8ce7b310c5 document how to work with generated columns 2025-02-17 15:06:18 +01:00
Benjamin Eberlei
5077ae41e5 Housekeeping 2025-02-15 23:25:34 +01:00
Benjamin Eberlei
8e1a27b8cc Explain deprecation in UPGRADE.md 2025-02-15 22:32:16 +01:00
Benjamin Eberlei
e7db1b005f Add ReadOnlyAccessorTest 2025-02-15 22:17:29 +01:00
Benjamin Eberlei
72ce662e45 Tests for ObjectCastPropertyAccessor and RawValuePropertyAccessor. 2025-02-15 22:09:36 +01:00
Benjamin Eberlei
673cf0d4d8 Add test for ObjectCastPropertyAccessor. 2025-02-15 21:45:01 +01:00
Benjamin Eberlei
1cae0534a0 Extract PropertyAccessorFactory, tests for enum and typednodefault accessors. 2025-02-15 21:38:09 +01:00
Benjamin Eberlei
6fb3083f63 Merge remote-tracking branch 'beberlei/PropertyHooks' into PropertyHooks 2025-02-15 00:02:45 +01:00
Benjamin Eberlei
68c17ca1bd Merge remote-tracking branch 'origin/3.4.x' into PropertyHooks 2025-02-15 00:01:50 +01:00
Benjamin Eberlei
82cf29407c Update src/Mapping/PropertyAccessors/PropertyAccessor.php
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2025-02-15 00:01:22 +01:00
Grégoire Paris
ae74be5e9d Merge pull request #11823 from doctrine/3.3.x-merge-up-into-3.4.x_lzhu6IBq
Merge release 3.3.2 into 3.4.x
2025-02-05 08:08:57 +01:00
Grégoire Paris
4163efd2f2 Merge pull request #11813 from VincentLanglet/queryType
[RFC] Expose QueryBuilder::getType
2025-01-29 11:54:45 +01:00
Vincent Langlet
d7ac6123ad Expose QueryType 2025-01-29 09:27:44 +01:00
Simon Podlipsky
bd260d1be8 feat: allow setting foreign key as deferrable 2025-01-26 13:06:55 +01:00
Grégoire Paris
cd1a52c7e4 Merge pull request #11808 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2025-01-25 11:29:59 +01:00
Alexander M. Turek
0d2cb6acd1 Fix CS (#11782) 2025-01-07 09:53:43 +01:00
Alexander M. Turek
327418a4b7 Merge branch '3.3.x' into 3.4.x
* 3.3.x:
  Update working-with-objects.rst (#7553)
  changed confusing negative wording (#11775)
2025-01-06 20:51:29 +01:00
Grégoire Paris
9f2b367081 Merge pull request #11776 from curry684/issue-9558
Respect referencedColumnName defaults in custom naming strategies
2024-12-25 00:20:07 +01:00
Niels Keurentjes
a9873c86bb Take hardcoded reference column name out of JoinColumn attribute
Previously, when using a custom naming strategy, explicitly declaring a JoinColumn required specifying the referencedColumnName always as it would default to id no matter the naming strategy. This PR changes it to be determines correctly.

Ref #9558
2024-12-23 19:44:56 +01:00
Grégoire Paris
8ebd98ee92 Merge pull request #11773 from doctrine/3.3.x-merge-up-into-3.4.x_xx7XyUCl
Merge release 3.3.1 into 3.4.x
2024-12-19 08:27:07 +01:00
Benjamin Eberlei
5a220078e9 Update PR with PHP Stan by fixing some and baselining other violations. 2024-12-08 21:11:41 +01:00
Grégoire Paris
a15543a2ce Merge pull request #11761 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2024-12-08 13:02:05 +01:00
Benjamin Eberlei
238fb74028 Add RawValuePropertyAccessor to see how it will look in 8.4, pre support for lazy objects. 2024-12-08 13:00:10 +01:00
Benjamin Eberlei
6ff2b130d3 Add comment to PropertyAccessor interface 2024-12-08 13:00:10 +01:00
Benjamin Eberlei
8c9bfca255 Fix wrong type, phpstan failure. 2024-12-08 13:00:10 +01:00
Benjamin Eberlei
c2a2386df9 suppress phpcs that cant be done 2024-12-08 13:00:10 +01:00
Benjamin Eberlei
2f98e11562 Remove last use of reflFields in core. 2024-12-08 13:00:10 +01:00
Benjamin Eberlei
073809cf5c Fixup EnumPropertyAccessor::toEnum 2024-12-08 13:00:10 +01:00
Benjamin Eberlei
e82690d256 More psalm to fix the errors. 2024-12-08 13:00:07 +01:00
Benjamin Eberlei
23c31aec51 Static analysis. 2024-12-08 12:57:19 +01:00
Benjamin Eberlei
622ba2dcc7 Mark all PropertyAccessor classes @internal. 2024-12-08 12:56:39 +01:00
Benjamin Eberlei
0c1cf853fc Address PHPStan issues. 2024-12-08 12:56:38 +01:00
Benjamin Eberlei
79d1f07fa2 Deprecate access to ClassMetadata::$reflFields. 2024-12-08 12:56:38 +01:00
Benjamin Eberlei
eba01f8d0e Style, missing getReflectionProperties()Property() that were renamed. 2024-12-08 12:56:38 +01:00
Benjamin Eberlei
bd292481bd Adjust test. 2024-12-08 12:56:38 +01:00
Benjamin Eberlei
fcc53b260f Use ClassMetadata::$propertyAccessors in all places. 2024-12-08 12:56:34 +01:00
Benjamin Eberlei
7d61a1e73f Fixes in LegacyReflectionFields. 2024-12-08 12:56:08 +01:00
Benjamin Eberlei
b3cffe2d12 Introduce LegacyReflectionFields abstraction, deriving from propertyAccessors at runtime. 2024-12-08 12:56:06 +01:00
Benjamin Eberlei
052c7d7698 Add all necessary accessors, adapting doctrine/persistence and ORM internal reflection properties. no tests. 2024-12-08 12:55:14 +01:00
Benjamin Eberlei
c2713adebc property hooks. 2024-12-08 12:55:10 +01:00
Grégoire Paris
51a984be3d Merge pull request #11758 from doctrine/3.3.x
Merge 3.3.x up into 3.4.x
2024-12-08 12:42:02 +01:00
Grégoire Paris
6007154484 Merge pull request #11746 from greg0ire/3.4.x
Merge 2.21.x up into 3.4.x
2024-12-04 07:52:51 +01:00
Grégoire Paris
22ce0aff37 Merge remote-tracking branch 'origin/2.21.x' into 3.4.x 2024-12-03 23:44:05 +01:00
Grégoire Paris
37051d57ce Merge pull request #11739 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-11-28 08:23:12 +01:00
Grégoire Paris
4563f2f9a7 Merge pull request #11737 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-11-27 22:10:21 +01:00
Grégoire Paris
91201c094a Merge pull request #11722 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-11-23 19:35:45 +01:00
Grégoire Paris
a4a15ad243 Merge pull request #11687 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-10-16 23:37:08 +02:00
123 changed files with 3099 additions and 495 deletions

View File

@@ -11,17 +11,23 @@
"slug": "latest",
"upcoming": true
},
{
"name": "3.5",
"branchName": "3.5.x",
"slug": "3.5",
"upcoming": true
},
{
"name": "3.4",
"branchName": "3.4.x",
"slug": "3.4",
"upcoming": true
"current": true
},
{
"name": "3.3",
"branchName": "3.3.x",
"slug": "3.3",
"current": true
"maintained": false
},
{
"name": "3.2",

View File

@@ -24,4 +24,4 @@ on:
jobs:
coding-standards:
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.2.2"
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.3.0"

View File

@@ -43,17 +43,27 @@ jobs:
- "pdo_sqlite"
deps:
- "highest"
native_lazy:
- "0"
include:
- php-version: "8.2"
dbal-version: "4@dev"
extension: "pdo_sqlite"
native_lazy: "0"
- php-version: "8.2"
dbal-version: "4@dev"
extension: "sqlite3"
native_lazy: "0"
- php-version: "8.1"
dbal-version: "default"
deps: "lowest"
extension: "pdo_sqlite"
native_lazy: "0"
- php-version: "8.4"
dbal-version: "default"
deps: "highest"
extension: "pdo_sqlite"
native_lazy: "1"
steps:
- name: "Checkout"
@@ -83,16 +93,18 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage-no-cache.xml"
env:
ENABLE_SECOND_LEVEL_CACHE: 0
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
- name: "Run PHPUnit with Second Level Cache"
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-cache.xml"
env:
ENABLE_SECOND_LEVEL_CACHE: 1
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
- name: "Upload coverage file"
uses: "actions/upload-artifact@v4"
with:
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-coverage"
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.native_lazy }}-coverage"
path: "coverage*.xml"

View File

@@ -17,4 +17,4 @@ on:
jobs:
documentation:
name: "Documentation"
uses: "doctrine/.github/.github/workflows/documentation.yml@7.2.2"
uses: "doctrine/.github/.github/workflows/documentation.yml@7.3.0"

View File

@@ -7,7 +7,7 @@ on:
jobs:
release:
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.2.2"
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.3.0"
secrets:
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}

View File

@@ -1,7 +1,7 @@
| [4.0.x][4.0] | [3.4.x][3.4] | [3.3.x][3.3] | [2.21.x][2.21] | [2.20.x][2.20] |
| [4.0.x][4.0] | [3.5.x][3.5] | [3.4.x][3.4] | [2.21.x][2.21] | [2.20.x][2.20] |
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
| [![Build status][4.0 image]][4.0] | [![Build status][3.4 image]][3.4] | [![Build status][3.3 image]][3.3] | [![Build status][2.21 image]][2.21] | [![Build status][2.20 image]][2.20] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.4 coverage image]][3.4 coverage] | [![Coverage Status][3.3 coverage image]][3.3 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
| [![Build status][4.0 image]][4.0] | [![Build status][3.5 image]][3.5] | [![Build status][3.4 image]][3.4] | [![Build status][2.21 image]][2.21] | [![Build status][2.20 image]][2.20] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.5 coverage image]][3.5 coverage] | [![Coverage Status][3.4 coverage image]][3.4 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence
for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features
@@ -20,14 +20,14 @@ without requiring unnecessary code duplication.
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
[3.5 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.5.x
[3.5]: https://github.com/doctrine/orm/tree/3.5.x
[3.5 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.5.x/graph/badge.svg
[3.5 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.5.x
[3.4 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.4.x
[3.4]: https://github.com/doctrine/orm/tree/3.4.x
[3.4 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.4.x/graph/badge.svg
[3.4 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.4.x
[3.3 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.3.x
[3.3]: https://github.com/doctrine/orm/tree/3.3.x
[3.3 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.3.x/graph/badge.svg
[3.3 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.3.x
[2.21 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.21.x
[2.21]: https://github.com/doctrine/orm/tree/2.21.x
[2.21 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.21.x/graph/badge.svg

View File

@@ -1,8 +1,28 @@
# Upgrade to 3.4.1
## BC BREAK: You can no longer use the `.*` notation to get all fields of an entity in a DTO
This feature was introduced in 3.4.0, and introduces several issues, so we
decide to remove it before it is used too widely.
# Upgrade to 3.4
## Discriminator Map class duplicates
Using the same class several times in a discriminator map is deprecated.
In 4.0, this will be an error.
## `Doctrine\ORM\Mapping\ClassMetadata::$reflFields` deprecated
To better support property hooks and lazy proxies in the future, `$reflFields` had to
be deprecated because we cannot use the PHP internal reflection API directly anymore.
The property was changed from an array to an object of type `LegacyReflectionFields`
that implements `ArrayAccess`.
Use the new `Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor` API and access
through `Doctrine\ORM\Mapping\ClassMetadata::$propertyAccessors` instead.
# Upgrade to 3.3
## Deprecate `DatabaseDriver`
@@ -13,7 +33,7 @@ The class `Doctrine\ORM\Mapping\Driver\DatabaseDriver` is deprecated without rep
Output walkers should implement the new `\Doctrine\ORM\Query\OutputWalker` interface and create
`Doctrine\ORM\Query\Exec\SqlFinalizer` instances instead of `Doctrine\ORM\Query\Exec\AbstractSqlExecutor`s.
The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the
The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the
`SqlFinalizer` can be kept in the query cache and used regardless of the actual `firstResult`/`maxResult` values.
Any operation dependent on `firstResult`/`maxResult` should take place within the `SqlFinalizer::createExecutor()`
method. Details can be found at https://github.com/doctrine/orm/pull/11188.
@@ -124,7 +144,7 @@ WARNING: This was relaxed in ORM 3.2 when partial was re-allowed for array-hydra
`Doctrine\ORM\Query::HINT_FORCE_PARTIAL_LOAD` are removed.
- `Doctrine\ORM\EntityManager*::getPartialReference()` is removed.
## BC BREAK: Enforce ArrayCollection Type on `\Doctrine\ORM\QueryBuilder::setParameters(ArrayCollection $parameters)`
## BC BREAK: Enforce ArrayCollection Type on `\Doctrine\ORM\QueryBuilder::setParameters(ArrayCollection $parameters)`
The argument $parameters can no longer be a key=>value array. Only ArrayCollection types are allowed.

3
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
composer.lock
vendor/
build/

24
docs/Makefile Normal file
View File

@@ -0,0 +1,24 @@
# Makefile for Doctrine ORM documentation
#
# You can set these variables from the command line.
DOCOPTS =
BUILD = vendor/bin/guides
BUILDDIR = build
# Internal variables.
ALLGUIDESOPTS = $(DOCOPTS) en/
.PHONY: help clean html
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
clean:
-rm -rf ./$(BUILDDIR)/*
html:
$(BUILD) $(ALLGUIDESOPTS) --output=$(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

View File

@@ -1,18 +1,22 @@
# Doctrine ORM Documentation
The documentation is written in [ReStructured Text](https://docutils.sourceforge.io/rst.html).
## How to Generate:
Using Ubuntu 14.04 LTS:
1. Run ./bin/install-dependencies.sh
2. Run ./bin/generate-docs.sh
In the `docs/` folder, run
It will generate the documentation into the build directory of the checkout.
composer update
Then compile the documentation with:
## Theme issues
make html
If you get a "Theme error", check if the `en/_theme` subdirectory is empty,
in which case you will need to run:
This will generate the documentation into the `build` subdirectory.
1. git submodule init
2. git submodule update
To browse the documentation, you need to run a webserver:
cd build/html
php -S localhost:8000
Now the documentation is available at [http://localhost:8000](http://localhost:8000).

View File

@@ -1,10 +0,0 @@
#!/bin/bash
EXECPATH=`dirname $0`
cd $EXECPATH
cd ..
rm build -Rf
sphinx-build en build
sphinx-build -b latex en build/pdf
rubber --into build/pdf --pdf build/pdf/Doctrine2ORM.tex

View File

@@ -1,2 +0,0 @@
#!/bin/bash
sudo apt-get update && sudo apt-get install -y python2.7 python-sphinx python-pygments

10
docs/composer.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "doctrine/orm-docs",
"description": "Documentation for the Object-Relational Mapper\"",
"type": "library",
"license": "MIT",
"require": {
"phpdocumentor/guides-cli": "1.7.1",
"phpdocumentor/filesystem": "1.7.1"
}
}

View File

@@ -1,89 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Doctrine2ORM.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Doctrine2ORM.qhc"
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@@ -0,0 +1,44 @@
Generated Columns
=================
Generated columns, sometimes also called virtual columns, are populated by
the database engine itself. They are a tool for performance optimization, to
avoid calculating a value on each query.
You can define generated columns on entities and have Doctrine map the values
to your entity.
Declaring a generated column
----------------------------
There is no explicit mapping instruction for generated columns. Instead, you
specify that the column should not be written to, and define a custom column
definition.
.. literalinclude:: generated-columns/Person.php
:language: php
* ``insertable``, ``updatable``: Setting these to false tells Doctrine to never
write this column - writing to a generated column would result in an error
from the database.
* ``columnDefinition``: We specify the full DDL to create the column. To allow
to use database specific features, this attribute does not use Doctrine Query
Language but native SQL. Note that you need to reference columns by their
database name (either explicitly set in the mapping or per the current
:doc:`naming strategy <../reference/namingstrategy>`).
Be aware that specifying a column definition makes the ``SchemaTool``
completely ignore all other configuration for this column. See also
:ref:`#[Column] <attrref_column>`
* ``generated``: Specifying that this column is always generated tells Doctrine
to update the field on the entity with the value from the database after
every write operation.
Advanced example: Extracting a value from a JSON structure
----------------------------------------------------------
Lets assume we have an entity that stores a blogpost as structured JSON.
To avoid extracting all titles on the fly when listing the posts, we create a
generated column with the field.
.. literalinclude:: generated-columns/Article.php
:language: php

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private int $id;
/**
* When working with Postgres, it is recommended to use the jsonb
* format for better performance.
*/
#[ORM\Column(options: ['jsonb' => true])]
private array $content;
/**
* Because we specify NOT NULL, inserting will fail if the content does
* not have a string in the title field.
*/
#[ORM\Column(
insertable: false,
updatable: false,
columnDefinition: "VARCHAR(255) generated always as (content->>'title') stored NOT NULL",
generated: 'ALWAYS',
)]
private string $title;
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Person
{
#[ORM\Column(type: 'string')]
private string $firstName;
#[ORM\Column(type: 'string', name: 'name')]
private string $lastName;
#[ORM\Column(
type: 'string',
insertable: false,
updatable: false,
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstName, ' ', name) stored NOT NULL",
generated: 'ALWAYS',
)]
private string $fullName;
}

View File

@@ -102,6 +102,7 @@ Cookbook
* **Patterns**:
:doc:`Aggregate Fields <cookbook/aggregate-fields>` \|
:doc:`Generated/Virtual Columns <cookbook/generated-columns>` \|
:doc:`Decorator Pattern <cookbook/decorator-pattern>` \|
:doc:`Strategy Pattern <cookbook/strategy-cookbook-introduction>`
@@ -121,4 +122,5 @@ Cookbook
* **Custom Datatypes**
:doc:`MySQL Enums <cookbook/mysql-enums>`
:doc:`Custom Mapping Types <cookbook/custom-mapping-types>`
:doc:`Advanced Field Value Conversion <cookbook/advanced-field-value-conversion-using-custom-mapping-types>`

View File

@@ -19,7 +19,7 @@ steps of configuration.
// ...
if ($applicationMode == "development") {
if ($applicationMode === "development") {
$queryCache = new ArrayAdapter();
$metadataCache = new ArrayAdapter();
} else {
@@ -32,13 +32,18 @@ steps of configuration.
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$config->setMetadataDriverImpl($driverImpl);
$config->setQueryCache($queryCache);
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
$config->setProxyNamespace('MyProject\Proxies');
if ($applicationMode == "development") {
$config->setAutoGenerateProxyClasses(true);
if (PHP_VERSION_ID > 80400) {
$config->enableNativeLazyObjects(true);
} else {
$config->setAutoGenerateProxyClasses(false);
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
$config->setProxyNamespace('MyProject\Proxies');
if ($applicationMode === "development") {
$config->setAutoGenerateProxyClasses(true);
} else {
$config->setAutoGenerateProxyClasses(false);
}
}
$connection = DriverManager::getConnection([
@@ -71,8 +76,25 @@ Configuration Options
The following sections describe all the configuration options
available on a ``Doctrine\ORM\Configuration`` instance.
Proxy Directory (**REQUIRED**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Native Lazy Objects (**OPTIONAL**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
With PHP 8.4 we recommend that you use native lazy objects instead of
the code generation approach using the ``symfony/var-exporter`` Ghost trait.
With Doctrine 4, the minimal requirement will become PHP 8.4 and native lazy objects
will become the only approach to lazy loading.
.. code-block:: php
<?php
$config->enableNativeLazyObjects(true);
Proxy Directory
~~~~~~~~~~~~~~~
Required except if you use native lazy objects with PHP 8.4.
This setting will be removed in the future.
.. code-block:: php
@@ -85,8 +107,11 @@ classes. For a detailed explanation on proxy classes and how they
are used in Doctrine, refer to the "Proxy Objects" section further
down.
Proxy Namespace (**REQUIRED**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxy Namespace
~~~~~~~~~~~~~~~
Required except if you use native lazy objects with PHP 8.4.
This setting will be removed in the future.
.. code-block:: php
@@ -200,6 +225,9 @@ deprecated ``Doctrine\DBAL\Logging\SQLLogger`` interface.
Auto-generating Proxy Classes (**OPTIONAL**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This setting is not required if you use native lazy objects with PHP 8.4
and will be removed in the future.
Proxy classes can either be generated manually through the Doctrine
Console or automatically at runtime by Doctrine. The configuration
option that controls this behavior is:

View File

@@ -214,12 +214,15 @@ Optional parameters:
- ``check``: Adds a check constraint type to the column (might not
be supported by all vendors).
- **columnDefinition**: DDL SQL snippet that starts after the column
- **columnDefinition**: Specify the DDL SQL snippet that starts after the column
name and specifies the complete (non-portable!) column definition.
This attribute allows to make use of advanced RMDBS features.
However you should make careful use of this feature and the
consequences. ``SchemaTool`` will not detect changes on the column correctly
anymore if you use ``columnDefinition``.
However, as this needs to be specified in the DDL native to the database,
the resulting schema changes are no longer portable. If you specify a
``columnDefinition``, the ``SchemaTool`` ignores all other attributes
that are normally used to build the definition DDL. Changes to the
``columnDefinition`` are not detected, you will need to manually create a
migration to apply changes.
Additionally you should remember that the ``type``
attribute still handles the conversion between PHP and Database
@@ -262,10 +265,11 @@ Examples:
)]
protected $loginCount;
// MySQL example: full_name char(41) GENERATED ALWAYS AS (concat(firstname,' ',lastname)),
// columnDefinition is raw SQL, not DQL. This example works for MySQL:
#[Column(
type: "string",
name: "user_fullname",
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstname,' ',lastname))",
insertable: false,
updatable: false
)]
@@ -366,7 +370,7 @@ Optional parameters:
- **type**: By default this is string.
- **length**: By default this is 255.
- **columnDefinition**: By default this is null the definition according to the type will be used. This option allows to override it.
- **columnDefinition**: Allows to override how the column is generated. See the "columnDefinition" attribute on :ref:`#[Column] <attrref_column>`
- **enumType**: By default this is `null`. Allows to map discriminatorColumn value to PHP enum
- **options**: See "options" attribute on :ref:`#[Column] <attrref_column>`.
@@ -672,13 +676,16 @@ Optional parameters:
- **unique**: Determines whether this relation is exclusive between the
affected entities and should be enforced as such on the database
constraint level. Defaults to false.
- **deferrable**: Determines whether this relation constraint can be deferred. Defaults to false.
- **nullable**: Determine whether the related entity is required, or if
null is an allowed state for the relation. Defaults to true.
- **onDelete**: Cascade Action (Database-level)
- **columnDefinition**: DDL SQL snippet that starts after the column
name and specifies the complete (non-portable!) column definition.
This attribute enables the use of advanced RMDBS features. Using
this attribute on ``#[JoinColumn]`` is necessary if you need slightly
This attribute enables the use of advanced RMDBS features. Note that you
need to reference columns by their database name (either explicitly set in
the mapping or per the current :doc:`naming strategy <namingstrategy>`).
Using this attribute on ``#[JoinColumn]`` is necessary if you need
different column definitions for joining columns, for example
regarding NULL/NOT NULL defaults. However by default a
"columnDefinition" attribute on :ref:`#[Column] <attrref_column>` also sets
@@ -1133,7 +1140,7 @@ Marker attribute that defines a specified column as version attribute used in
an :ref:`optimistic locking <transactions-and-concurrency_optimistic-locking>`
scenario. It only works on :ref:`#[Column] <attrref_column>` attributes that have
the type ``integer`` or ``datetime``. Setting ``#[Version]`` on a property with
:ref:`#[Id <attrref_id>` is not supported.
:ref:`#[Id] <attrref_id>` is not supported.
Example:

View File

@@ -214,6 +214,8 @@ These are the "automatic" mapping rules:
| Any other type | ``Types::STRING`` |
+-----------------------+-------------------------------+
.. versionadded:: 2.11
As of version 2.11 Doctrine can also automatically map typed properties using a
PHP 8.1 enum to set the right ``type`` and ``enumType``.
@@ -224,6 +226,70 @@ and a custom ``Doctrine\ORM\Mapping\TypedFieldMapper`` implementation.
:doc:`Read more about TypedFieldMapper <typedfieldmapper>`.
Property Hooks
--------------
.. versionadded:: 3.4
Doctrine supports mapping hooked properties as long as they have a backed property
and are not virtual.
.. configuration-block::
.. code-block:: attribute
<?php
use Doctrine\ORM\Mapping\Column;
use Doctrine\DBAL\Types\Types;
#[Entity]
class Message
{
#[Column(type: Types::INTEGER)]
private $id;
#[Column(type: Types::STRING)]
public string $language = 'de' {
// Override the "read" action with arbitrary logic.
get => strtoupper($this->language);
// Override the "write" action with arbitrary logic.
set {
$this->language = strtolower($value);
}
}
}
.. code-block:: xml
<doctrine-mapping>
<entity name="Message">
<field name="id" type="integer" />
<field name="language" />
</entity>
</doctrine-mapping>
If you attempt to map a virtual property with ``#[Column]`` an exception will be thrown.
Some caveats apply to the use of property hooks, as they behave differently when accessing the property through
the entity or directly through DQL/EntityRepository. Because the property hook can modify the value of the property in a way
that value and raw value are different, you have to use the raw value representation when querying for the property.
.. code-block:: php
<?php
$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder->select('m')
->from(Message::class, 'm')
->where('m.language = :language')
->setParameter('language', 'de'); // Use lower case here for raw value representation
$query = $queryBuilder->getQuery();
$result = $query->getResult();
$messageRepository = $entityManager->getRepository(Message::class);
$deMessages = $messageRepository->findBy(['language' => 'de']); // Use lower case here for raw value representation
.. _reference-mapping-types:
Doctrine Mapping Types

View File

@@ -588,7 +588,7 @@ And then use the ``NEW`` DQL keyword :
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, a.city, SUM(o.value)) FROM Customer c JOIN c.email e JOIN c.address a JOIN c.orders o GROUP BY c');
$users = $query->getResult(); // array of CustomerDTO
You can also nest several DTO :
You can also nest several DTO :
.. code-block:: php
@@ -674,6 +674,16 @@ The ``NAMED`` keyword must precede all DTO you want to instantiate :
If two arguments have the same name, a ``DuplicateFieldException`` is thrown.
If a field cannot be matched with a property name, a ``NoMatchingPropertyException`` is thrown. This typically happens when using functions without aliasing them.
You can hydrate an entity nested in a DTO :
.. code-block:: php
<?php
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, a AS address) FROM Customer c JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO
// CustomerDTO => {name : 'DOE', email: null, address : {city: 'New York', zip: '10011', address: 'Abbey Road'}
Using INDEX BY
~~~~~~~~~~~~~~
@@ -1697,12 +1707,13 @@ Select Expressions
.. code-block:: php
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression | EntityAsDtoArgumentExpression) ["AS" AliasResultVariable]
EntityAsDtoArgumentExpression ::= IdentificationVariable
Conditional Expressions
~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -64,6 +64,7 @@
cookbook/decorator-pattern
cookbook/dql-custom-walkers
cookbook/dql-user-defined-functions
cookbook/generated-columns
cookbook/implementing-arrayaccess-for-domain-objects
cookbook/resolve-target-entity-listener
cookbook/sql-table-prefixes

View File

@@ -583,7 +583,7 @@ parameters:
path: src/EntityManager.php
-
message: '#^Method Doctrine\\ORM\\EntityManager\:\:getReference\(\) should return \(T of object\)\|null but returns Doctrine\\ORM\\Proxy\\InternalProxy\.$#'
message: '#^Method Doctrine\\ORM\\EntityManager\:\:getReference\(\) should return \(T of object\)\|null but returns object\.$#'
identifier: return.type
count: 1
path: src/EntityManager.php
@@ -984,12 +984,6 @@ parameters:
count: 1
path: src/Mapping/ClassMetadata.php
-
message: '#^Parameter \#1 \$class of method Doctrine\\Persistence\\Mapping\\ReflectionService\:\:getAccessibleProperty\(\) expects class\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Mapping/ClassMetadata.php
-
message: '#^Parameter \#1 \$mapping of method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:validateAndCompleteTypedAssociationMapping\(\) expects array\{type\: 1\|2\|4\|8, fieldName\: string, targetEntity\?\: class\-string\}, non\-empty\-array\<string, mixed\> given\.$#'
identifier: argument.type
@@ -1032,18 +1026,6 @@ parameters:
count: 2
path: src/Mapping/ClassMetadata.php
-
message: '#^Parameter \#2 \$class of method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:getAccessibleProperty\(\) expects class\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Mapping/ClassMetadata.php
-
message: '#^Parameter \#3 \$embeddedClass of class Doctrine\\ORM\\Mapping\\ReflectionEmbeddedProperty constructor expects class\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Mapping/ClassMetadata.php
-
message: '#^Property Doctrine\\ORM\\Mapping\\ClassMetadata\:\:\$customRepositoryClassName with generic class Doctrine\\ORM\\EntityRepository does not specify its types\: T$#'
identifier: missingType.generics
@@ -1578,12 +1560,36 @@ parameters:
count: 1
path: src/Mapping/JoinTableMapping.php
-
message: '#^Method Doctrine\\ORM\\Mapping\\LegacyReflectionFields\:\:__construct\(\) has parameter \$classMetadata with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: src/Mapping/LegacyReflectionFields.php
-
message: '#^Parameter \#1 \$class of method Doctrine\\Persistence\\Mapping\\ReflectionService\:\:getAccessibleProperty\(\) expects class\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Mapping/LegacyReflectionFields.php
-
message: '#^Method Doctrine\\ORM\\Mapping\\MappedSuperclass\:\:__construct\(\) has parameter \$repositoryClass with generic class Doctrine\\ORM\\EntityRepository but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: src/Mapping/MappedSuperclass.php
-
message: '#^Method Doctrine\\ORM\\Mapping\\PropertyAccessors\\EnumPropertyAccessor\:\:toEnum\(\) should return array\<BackedEnum\>\|BackedEnum but returns array\<BackedEnum\|int\|string\>\.$#'
identifier: return.type
count: 1
path: src/Mapping/PropertyAccessors/EnumPropertyAccessor.php
-
message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(BackedEnum\|int\|string\)\: mixed\)\|null, array\{class\-string\<BackedEnum\>, ''from''\} given\.$#'
identifier: argument.type
count: 1
path: src/Mapping/PropertyAccessors/EnumPropertyAccessor.php
-
message: '#^Method Doctrine\\ORM\\Mapping\\QuoteStrategy\:\:getColumnAlias\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
@@ -2394,12 +2400,6 @@ parameters:
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '#^Method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:getProxy\(\) return type with generic interface Doctrine\\ORM\\Proxy\\InternalProxy does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: src/Proxy/ProxyFactory.php
-
message: '#^Method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:loadProxyClass\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
@@ -2868,12 +2868,6 @@ parameters:
count: 1
path: src/Query/SqlWalker.php
-
message: '#^Property Doctrine\\ORM\\Query\\SqlWalker\:\:\$selectedClasses \(array\<string, array\{class\: Doctrine\\ORM\\Mapping\\ClassMetadata, dqlAlias\: string, resultAlias\: string\|null\}\>\) does not accept non\-empty\-array\<int\|string, array\{class\: Doctrine\\ORM\\Mapping\\ClassMetadata, dqlAlias\: mixed, resultAlias\: string\|null\}\>\.$#'
identifier: assign.propertyType
count: 1
path: src/Query/SqlWalker.php
-
message: '#^Property Doctrine\\ORM\\Query\\SqlWalker\:\:\$selectedClasses with generic class Doctrine\\ORM\\Mapping\\ClassMetadata does not specify its types\: T$#'
identifier: missingType.generics

View File

@@ -45,7 +45,7 @@ parameters:
path: src/UnitOfWork.php
-
message: '~^Parameter #1 \$command of method Symfony\\Component\\Console\\Application::add\(\) expects Symfony\\Component\\Console\\Command\\Command, Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand given\.$~'
message: '~^Parameter #2 \$command of static method Doctrine\\ORM\\Tools\\Console\\ConsoleRunner::addCommandToApplication\(\) expects Symfony\\Component\\Console\\Command\\Command, Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand given\.$~'
path: src/Tools/Console/ConsoleRunner.php
-

View File

@@ -30,6 +30,8 @@ use function class_exists;
use function is_a;
use function strtolower;
use const PHP_VERSION_ID;
/**
* Configuration container for all configuration options of Doctrine.
* It combines all configuration options from DBAL & ORM.
@@ -593,6 +595,20 @@ class Configuration extends \Doctrine\DBAL\Configuration
$this->attributes['schemaIgnoreClasses'] = $schemaIgnoreClasses;
}
public function isNativeLazyObjectsEnabled(): bool
{
return $this->attributes['nativeLazyObjects'] ?? false;
}
public function enableNativeLazyObjects(bool $nativeLazyObjects): void
{
if (PHP_VERSION_ID < 80400) {
throw new LogicException('Lazy loading proxies require PHP 8.4 or higher.');
}
$this->attributes['nativeLazyObjects'] = $nativeLazyObjects;
}
/**
* To be deprecated in 3.1.0
*

View File

@@ -19,6 +19,7 @@ use LogicException;
use ReflectionClass;
use function array_key_exists;
use function array_keys;
use function array_map;
use function array_merge;
use function count;
@@ -348,14 +349,29 @@ abstract class AbstractHydrator
}
}
$nestedEntities = [];
/**@var string $argAlias */
foreach ($this->resultSetMapping()->nestedNewObjectArguments as ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex, 'argAlias' => $argAlias]) {
if (array_key_exists($argAlias, $rowData['newObjects'])) {
ksort($rowData['newObjects'][$argAlias]['args']);
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $rowData['newObjects'][$argAlias]['class']->newInstanceArgs($rowData['newObjects'][$argAlias]['args']);
unset($rowData['newObjects'][$argAlias]);
} elseif (array_key_exists($argAlias, $rowData['data'])) {
if (! array_key_exists($argAlias, $nestedEntities)) {
$nestedEntities[$argAlias] = '';
$rowData['data'][$argAlias] = $this->hydrateNestedEntity($rowData['data'][$argAlias], $argAlias);
}
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $rowData['data'][$argAlias];
} else {
throw new LogicException($argAlias . ' does not exist');
}
}
foreach (array_keys($nestedEntities) as $entity) {
unset($rowData['data'][$entity]);
}
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
ksort($rowData['newObjects'][$objIndex]['args']);
$obj = $rowData['newObjects'][$objIndex]['class']->newInstanceArgs($rowData['newObjects'][$objIndex]['args']);
@@ -366,6 +382,12 @@ abstract class AbstractHydrator
return $rowData;
}
/** @param mixed[] $data pre-hydrated SQL Result Row. */
protected function hydrateNestedEntity(array $data, string $dqlAlias): mixed
{
return $data;
}
/**
* Processes a row of the result set.
*

View File

@@ -70,6 +70,10 @@ class ObjectHydrator extends AbstractHydrator
$parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
if (! isset($this->resultSetMapping()->aliasMap[$parent])) {
if (isset($this->resultSetMapping()->nestedEntities[$dqlAlias])) {
continue;
}
throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent);
}
@@ -171,7 +175,7 @@ class ObjectHydrator extends AbstractHydrator
): PersistentCollection {
$oid = spl_object_id($entity);
$relation = $class->associationMappings[$fieldName];
$value = $class->reflFields[$fieldName]->getValue($entity);
$value = $class->propertyAccessors[$fieldName]->getValue($entity);
if ($value === null || is_array($value)) {
$value = new ArrayCollection((array) $value);
@@ -186,7 +190,7 @@ class ObjectHydrator extends AbstractHydrator
);
$value->setOwner($entity, $relation);
$class->reflFields[$fieldName]->setValue($entity, $value);
$class->propertyAccessors[$fieldName]->setValue($entity, $value);
$this->uow->setOriginalEntityProperty($oid, $fieldName, $value);
$this->initializedCollections[$oid . $fieldName] = $value;
@@ -346,7 +350,7 @@ class ObjectHydrator extends AbstractHydrator
$parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parentAlias]];
$relationField = $this->resultSetMapping()->relationMap[$dqlAlias];
$relation = $parentClass->associationMappings[$relationField];
$reflField = $parentClass->reflFields[$relationField];
$reflField = $parentClass->propertyAccessors[$relationField];
// Get a reference to the parent object to which the joined element belongs.
if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parentAlias])) {
@@ -446,13 +450,13 @@ class ObjectHydrator extends AbstractHydrator
if ($relation->inversedBy !== null) {
$inverseAssoc = $targetClass->associationMappings[$relation->inversedBy];
if ($inverseAssoc->isToOne()) {
$targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject);
$targetClass->propertyAccessors[$inverseAssoc->fieldName]->setValue($element, $parentObject);
$this->uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc->fieldName, $parentObject);
}
}
} else {
// For sure bidirectional, as there is no inverse side in unidirectional mappings
$targetClass->reflFields[$relation->mappedBy]->setValue($element, $parentObject);
$targetClass->propertyAccessors[$relation->mappedBy]->setValue($element, $parentObject);
$this->uow->setOriginalEntityProperty(spl_object_id($element), $relation->mappedBy, $parentObject);
}
@@ -569,6 +573,16 @@ class ObjectHydrator extends AbstractHydrator
}
}
/** @param mixed[] $data pre-hydrated SQL Result Row. */
protected function hydrateNestedEntity(array $data, string $dqlAlias): mixed
{
if (isset($this->resultSetMapping()->nestedEntities[$dqlAlias])) {
return $this->getEntity($data, $dqlAlias);
}
return $data;
}
/**
* When executed in a hydrate() loop we may have to clear internal state to
* decrease memory consumption.

View File

@@ -135,7 +135,13 @@ abstract class AssociationMapping implements ArrayAccess
continue;
}
assert($mapping instanceof ManyToManyOwningSideMapping);
if (! $mapping instanceof ManyToManyOwningSideMapping) {
throw new MappingException(
"Mapping error on field '" .
$mapping->fieldName . "' in " . $mapping->sourceEntity .
" : 'joinTable' can only be set on many-to-many owning side.",
);
}
$mapping->joinTable = JoinTableMapping::fromMappingArray($value);

View File

@@ -14,9 +14,12 @@ use Doctrine\Instantiator\InstantiatorInterface;
use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Id\AbstractIdGenerator;
use Doctrine\ORM\Mapping\PropertyAccessors\EmbeddablePropertyAccessor;
use Doctrine\ORM\Mapping\PropertyAccessors\EnumPropertyAccessor;
use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor;
use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessorFactory;
use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
use Doctrine\Persistence\Mapping\ReflectionService;
use Doctrine\Persistence\Reflection\EnumReflectionProperty;
use InvalidArgumentException;
use LogicException;
use ReflectionClass;
@@ -57,8 +60,6 @@ use function strtolower;
use function trait_exists;
use function trim;
use const PHP_VERSION_ID;
/**
* A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata
* of an entity and its associations.
@@ -543,9 +544,12 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
/**
* The ReflectionProperty instances of the mapped class.
*
* @var array<string, ReflectionProperty|null>
* @var LegacyReflectionFields|array<string, ReflectionProperty>
*/
public array $reflFields = [];
public LegacyReflectionFields|array $reflFields = [];
/** @var array<string, PropertyAccessors\PropertyAccessor> */
public array $propertyAccessors = [];
private InstantiatorInterface|null $instantiator = null;
@@ -569,14 +573,24 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
/**
* Gets the ReflectionProperties of the mapped class.
*
* @return ReflectionProperty[]|null[] An array of ReflectionProperty instances.
* @phpstan-return array<ReflectionProperty|null>
* @return LegacyReflectionFields|ReflectionProperty[] An array of ReflectionProperty instances.
* @phpstan-return LegacyReflectionFields|array<string, ReflectionProperty>
*/
public function getReflectionProperties(): array
public function getReflectionProperties(): array|LegacyReflectionFields
{
return $this->reflFields;
}
/**
* Gets the ReflectionProperties of the mapped class.
*
* @return PropertyAccessor[] An array of PropertyAccessor instances.
*/
public function getPropertyAccessors(): array
{
return $this->propertyAccessors;
}
/**
* Gets a ReflectionProperty for a specific field of the mapped class.
*/
@@ -585,11 +599,12 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
return $this->reflFields[$name];
}
/**
* Gets the ReflectionProperty for the single identifier field.
*
* @throws BadMethodCallException If the class has a composite identifier.
*/
public function getPropertyAccessor(string $name): PropertyAccessor|null
{
return $this->propertyAccessors[$name] ?? null;
}
/** @throws BadMethodCallException If the class has a composite identifier. */
public function getSingleIdReflectionProperty(): ReflectionProperty|null
{
if ($this->isIdentifierComposite) {
@@ -599,6 +614,16 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
return $this->reflFields[$this->identifier[0]];
}
/** @throws BadMethodCallException If the class has a composite identifier. */
public function getSingleIdPropertyAccessor(): PropertyAccessor|null
{
if ($this->isIdentifierComposite) {
throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.');
}
return $this->propertyAccessors[$this->identifier[0]];
}
/**
* Extracts the identifier values of an entity of this class.
*
@@ -613,7 +638,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
$id = [];
foreach ($this->identifier as $idField) {
$value = $this->reflFields[$idField]->getValue($entity);
$value = $this->propertyAccessors[$idField]->getValue($entity);
if ($value !== null) {
$id[$idField] = $value;
@@ -624,7 +649,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
}
$id = $this->identifier[0];
$value = $this->reflFields[$id]->getValue($entity);
$value = $this->propertyAccessors[$id]->getValue($entity);
if ($value === null) {
return [];
@@ -643,7 +668,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
public function setIdentifierValues(object $entity, array $id): void
{
foreach ($id as $idField => $idValue) {
$this->reflFields[$idField]->setValue($entity, $idValue);
$this->propertyAccessors[$idField]->setValue($entity, $idValue);
}
}
@@ -652,7 +677,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
*/
public function setFieldValue(object $entity, string $field, mixed $value): void
{
$this->reflFields[$field]->setValue($entity, $value);
$this->propertyAccessors[$field]->setValue($entity, $value);
}
/**
@@ -660,7 +685,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
*/
public function getFieldValue(object $entity, string $field): mixed
{
return $this->reflFields[$field]->getValue($entity);
return $this->propertyAccessors[$field]->getValue($entity);
}
/**
@@ -794,76 +819,74 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
{
// Restore ReflectionClass and properties
$this->reflClass = $reflService->getClass($this->name);
$this->reflFields = new LegacyReflectionFields($this, $reflService);
$this->instantiator = $this->instantiator ?: new Instantiator();
$parentReflFields = [];
$parentAccessors = [];
foreach ($this->embeddedClasses as $property => $embeddedClass) {
if (isset($embeddedClass->declaredField)) {
assert($embeddedClass->originalField !== null);
$childProperty = $this->getAccessibleProperty(
$reflService,
$childAccessor = PropertyAccessorFactory::createPropertyAccessor(
$this->embeddedClasses[$embeddedClass->declaredField]->class,
$embeddedClass->originalField,
);
assert($childProperty !== null);
$parentReflFields[$property] = new ReflectionEmbeddedProperty(
$parentReflFields[$embeddedClass->declaredField],
$childProperty,
$parentAccessors[$property] = new EmbeddablePropertyAccessor(
$parentAccessors[$embeddedClass->declaredField],
$childAccessor,
$this->embeddedClasses[$embeddedClass->declaredField]->class,
);
continue;
}
$fieldRefl = $this->getAccessibleProperty(
$reflService,
$accessor = PropertyAccessorFactory::createPropertyAccessor(
$embeddedClass->declared ?? $this->name,
$property,
);
$parentReflFields[$property] = $fieldRefl;
$this->reflFields[$property] = $fieldRefl;
$parentAccessors[$property] = $accessor;
$this->propertyAccessors[$property] = $accessor;
}
foreach ($this->fieldMappings as $field => $mapping) {
if (isset($mapping->declaredField) && isset($parentReflFields[$mapping->declaredField])) {
if (isset($mapping->declaredField) && isset($parentAccessors[$mapping->declaredField])) {
assert($mapping->originalField !== null);
assert($mapping->originalClass !== null);
$childProperty = $this->getAccessibleProperty($reflService, $mapping->originalClass, $mapping->originalField);
assert($childProperty !== null);
$accessor = PropertyAccessorFactory::createPropertyAccessor($mapping->originalClass, $mapping->originalField);
if (isset($mapping->enumType)) {
$childProperty = new EnumReflectionProperty(
$childProperty,
if ($mapping->enumType !== null) {
$accessor = new EnumPropertyAccessor(
$accessor,
$mapping->enumType,
);
}
$this->reflFields[$field] = new ReflectionEmbeddedProperty(
$parentReflFields[$mapping->declaredField],
$childProperty,
$this->propertyAccessors[$field] = new EmbeddablePropertyAccessor(
$parentAccessors[$mapping->declaredField],
$accessor,
$mapping->originalClass,
);
continue;
}
$this->reflFields[$field] = isset($mapping->declared)
? $this->getAccessibleProperty($reflService, $mapping->declared, $field)
: $this->getAccessibleProperty($reflService, $this->name, $field);
$this->propertyAccessors[$field] = isset($mapping->declared)
? PropertyAccessorFactory::createPropertyAccessor($mapping->declared, $field)
: PropertyAccessorFactory::createPropertyAccessor($this->name, $field);
if (isset($mapping->enumType) && $this->reflFields[$field] !== null) {
$this->reflFields[$field] = new EnumReflectionProperty(
$this->reflFields[$field],
if ($mapping->enumType !== null) {
$this->propertyAccessors[$field] = new EnumPropertyAccessor(
$this->propertyAccessors[$field],
$mapping->enumType,
);
}
}
foreach ($this->associationMappings as $field => $mapping) {
$this->reflFields[$field] = isset($mapping->declared)
? $this->getAccessibleProperty($reflService, $mapping->declared, $field)
: $this->getAccessibleProperty($reflService, $this->name, $field);
$this->propertyAccessors[$field] = isset($mapping->declared)
? PropertyAccessorFactory::createPropertyAccessor($mapping->declared, $field)
: PropertyAccessorFactory::createPropertyAccessor($this->name, $field);
}
}
@@ -2659,26 +2682,4 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
return $sequencePrefix;
}
/** @phpstan-param class-string $class */
private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ReflectionProperty|null
{
$reflectionProperty = $reflService->getAccessibleProperty($class, $field);
if ($reflectionProperty?->isReadOnly()) {
$declaringClass = $reflectionProperty->class;
if ($declaringClass !== $class) {
$reflectionProperty = $reflService->getAccessibleProperty($declaringClass, $field);
}
if ($reflectionProperty !== null) {
$reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
}
}
if (PHP_VERSION_ID >= 80400 && $reflectionProperty !== null && count($reflectionProperty->getHooks()) > 0) {
throw new LogicException('Doctrine ORM does not support property hooks in this version. Check https://github.com/doctrine/orm/issues/11624 for details of versions that support property hooks.');
}
return $reflectionProperty;
}
}

View File

@@ -41,6 +41,8 @@ use function strlen;
use function strtolower;
use function substr;
use const PHP_VERSION_ID;
/**
* The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
* metadata mapping information of a class which describes how a class should be mapped
@@ -440,8 +442,8 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
$subClass->addInheritedFieldMapping($subClassMapping);
}
foreach ($parentClass->reflFields as $name => $field) {
$subClass->reflFields[$name] = $field;
foreach ($parentClass->propertyAccessors as $name => $field) {
$subClass->propertyAccessors[$name] = $field;
}
}
@@ -699,6 +701,18 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
protected function wakeupReflection(ClassMetadataInterface $class, ReflectionService $reflService): void
{
$class->wakeupReflection($reflService);
if (PHP_VERSION_ID < 80400) {
return;
}
foreach ($class->propertyAccessors as $propertyAccessor) {
$property = $propertyAccessor->getUnderlyingReflector();
if ($property->isVirtual()) {
throw MappingException::mappingVirtualPropertyNotAllowed($class->name, $property->getName());
}
}
}
protected function initializeReflection(ClassMetadataInterface $class, ReflectionService $reflService): void

View File

@@ -684,6 +684,7 @@ class AttributeDriver implements MappingDriver
{
$mapping = [
'name' => $joinColumn->name,
'deferrable' => $joinColumn->deferrable,
'unique' => $joinColumn->unique,
'nullable' => $joinColumn->nullable,
'onDelete' => $joinColumn->onDelete,

View File

@@ -54,6 +54,7 @@ final class FieldMapping implements ArrayAccess
*/
public string|null $inherited = null;
/** @var class-string|null */
public string|null $originalClass = null;
public string|null $originalField = null;
public bool|null $quoted = null;
@@ -101,7 +102,7 @@ final class FieldMapping implements ArrayAccess
* scale?: int|null,
* unique?: bool|null,
* inherited?: string|null,
* originalClass?: string|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* quoted?: bool|null,
* declared?: string|null,

View File

@@ -13,6 +13,7 @@ final class JoinColumnMapping implements ArrayAccess
{
use ArrayAccessImplementation;
public bool|null $deferrable = null;
public bool|null $unique = null;
public bool|null $quoted = null;
public string|null $fieldName = null;
@@ -33,7 +34,7 @@ final class JoinColumnMapping implements ArrayAccess
* @param array<string, mixed> $mappingArray
* @phpstan-param array{
* name: string,
* referencedColumnName: string,
* referencedColumnName: string|null,
* unique?: bool|null,
* quoted?: bool|null,
* fieldName?: string|null,
@@ -66,7 +67,7 @@ final class JoinColumnMapping implements ArrayAccess
}
}
foreach (['unique', 'quoted', 'nullable'] as $boolKey) {
foreach (['deferrable', 'unique', 'quoted', 'nullable'] as $boolKey) {
if ($this->$boolKey !== null) {
$serialized[] = $boolKey;
}

View File

@@ -9,7 +9,8 @@ trait JoinColumnProperties
/** @param array<string, mixed> $options */
public function __construct(
public readonly string|null $name = null,
public readonly string $referencedColumnName = 'id',
public readonly string|null $referencedColumnName = null,
public readonly bool $deferrable = false,
public readonly bool $unique = false,
public readonly bool $nullable = true,
public readonly mixed $onDelete = null,

View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use ArrayAccess;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Persistence\Mapping\ReflectionService;
use Doctrine\Persistence\Reflection\EnumReflectionProperty;
use Generator;
use IteratorAggregate;
use OutOfBoundsException;
use ReflectionProperty;
use Traversable;
use function array_keys;
use function assert;
use function is_string;
use function str_contains;
use function str_replace;
/**
* @template-implements ArrayAccess<string, ReflectionProperty|null>
* @template-implements IteratorAggregate<string, ReflectionProperty|null>
*/
class LegacyReflectionFields implements ArrayAccess, IteratorAggregate
{
/** @var array<string, ReflectionProperty|null> */
private array $reflFields = [];
public function __construct(private ClassMetadata $classMetadata, private ReflectionService $reflectionService)
{
}
/** @param string $offset */
public function offsetExists($offset): bool // phpcs:ignore
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11659',
'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.',
);
return isset($this->classMetadata->propertyAccessors[$offset]);
}
/**
* @param string $field
*
* @psalm-suppress LessSpecificImplementedReturnType
*/
public function offsetGet($field): mixed // phpcs:ignore
{
if (isset($this->reflFields[$field])) {
return $this->reflFields[$field];
}
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11659',
'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.',
);
if (isset($this->classMetadata->propertyAccessors[$field])) {
$fieldName = str_contains($field, '.') ? $this->classMetadata->fieldMappings[$field]->originalField : $field;
$className = $this->classMetadata->name;
assert(is_string($fieldName));
if (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->originalClass !== null) {
$className = $this->classMetadata->fieldMappings[$field]->originalClass;
} elseif (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->declared !== null) {
$className = $this->classMetadata->fieldMappings[$field]->declared;
} elseif (isset($this->classMetadata->associationMappings[$field]) && $this->classMetadata->associationMappings[$field]->declared !== null) {
$className = $this->classMetadata->associationMappings[$field]->declared;
} elseif (isset($this->classMetadata->embeddedClasses[$field]) && $this->classMetadata->embeddedClasses[$field]->declared !== null) {
$className = $this->classMetadata->embeddedClasses[$field]->declared;
}
/** @psalm-suppress ArgumentTypeCoercion */
$this->reflFields[$field] = $this->getAccessibleProperty($className, $fieldName);
if (isset($this->classMetadata->fieldMappings[$field])) {
if ($this->classMetadata->fieldMappings[$field]->enumType !== null) {
$this->reflFields[$field] = new EnumReflectionProperty(
$this->reflFields[$field],
$this->classMetadata->fieldMappings[$field]->enumType,
);
}
if ($this->classMetadata->fieldMappings[$field]->originalField !== null) {
$parentField = str_replace('.' . $fieldName, '', $field);
$originalClass = $this->classMetadata->fieldMappings[$field]->originalClass;
if (! str_contains($parentField, '.')) {
$parentClass = $this->classMetadata->name;
} else {
$parentClass = $this->classMetadata->fieldMappings[$parentField]->originalClass;
}
/** @psalm-var class-string $parentClass */
/** @psalm-var class-string $originalClass */
$this->reflFields[$field] = new ReflectionEmbeddedProperty(
$this->getAccessibleProperty($parentClass, $parentField),
$this->reflFields[$field],
$originalClass,
);
}
}
return $this->reflFields[$field];
}
throw new OutOfBoundsException('Unknown field: ' . $this->classMetadata->name . ' ::$' . $field);
}
/**
* @param string $offset
* @param ReflectionProperty $value
*/
public function offsetSet($offset, $value): void // phpcs:ignore
{
$this->reflFields[$offset] = $value;
}
/** @param string $offset */
public function offsetUnset($offset): void // phpcs:ignore
{
unset($this->reflFields[$offset]);
}
/** @psalm-param class-string $class */
private function getAccessibleProperty(string $class, string $field): ReflectionProperty
{
$reflectionProperty = $this->reflectionService->getAccessibleProperty($class, $field);
assert($reflectionProperty !== null);
if ($reflectionProperty->isReadOnly()) {
$declaringClass = $reflectionProperty->class;
if ($declaringClass !== $class) {
$reflectionProperty = $this->reflectionService->getAccessibleProperty($declaringClass, $field);
assert($reflectionProperty !== null);
}
$reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
}
return $reflectionProperty;
}
/** @return Generator<string, ReflectionProperty> */
public function getIterator(): Traversable
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11659',
'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.',
);
$keys = array_keys($this->classMetadata->propertyAccessors);
foreach ($keys as $key) {
yield $key => $this->offsetGet($key);
}
}
}

View File

@@ -61,10 +61,14 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
{
if (isset($mappingArray['joinTable']['joinColumns'])) {
foreach ($mappingArray['joinTable']['joinColumns'] as $key => $joinColumn) {
if (empty($joinColumn['referencedColumnName'])) {
$mappingArray['joinTable']['joinColumns'][$key]['referencedColumnName'] = $namingStrategy->referenceColumnName();
}
if (empty($joinColumn['name'])) {
$mappingArray['joinTable']['joinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName(
$mappingArray['sourceEntity'],
$joinColumn['referencedColumnName'] ?? null,
$joinColumn['referencedColumnName'] ?? $namingStrategy->referenceColumnName(),
);
}
}
@@ -72,10 +76,14 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
if (isset($mappingArray['joinTable']['inverseJoinColumns'])) {
foreach ($mappingArray['joinTable']['inverseJoinColumns'] as $key => $joinColumn) {
if (empty($joinColumn['referencedColumnName'])) {
$mappingArray['joinTable']['inverseJoinColumns'][$key]['referencedColumnName'] = $namingStrategy->referenceColumnName();
}
if (empty($joinColumn['name'])) {
$mappingArray['joinTable']['inverseJoinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName(
$mappingArray['targetEntity'],
$joinColumn['referencedColumnName'] ?? null,
$joinColumn['referencedColumnName'] ?? $namingStrategy->referenceColumnName(),
);
}
}

View File

@@ -688,4 +688,13 @@ EXCEPTION
$entityName,
));
}
public static function mappingVirtualPropertyNotAllowed(string $entityName, string $propertyName): self
{
return new self(sprintf(
'Mapping virtual property "%s" on entity "%s" is not allowed.',
$propertyName,
$entityName,
));
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use Doctrine\Instantiator\Instantiator;
use ReflectionProperty;
/** @internal */
class EmbeddablePropertyAccessor implements PropertyAccessor
{
private static Instantiator|null $instantiator = null;
public function __construct(
private PropertyAccessor $parent,
private PropertyAccessor $child,
/** @var class-string */
private string $embeddedClass,
) {
}
public function setValue(object $object, mixed $value): void
{
$embeddedObject = $this->parent->getValue($object);
if ($embeddedObject === null) {
self::$instantiator ??= new Instantiator();
$embeddedObject = self::$instantiator->instantiate($this->embeddedClass);
$this->parent->setValue($object, $embeddedObject);
}
$this->child->setValue($embeddedObject, $value);
}
public function getValue(object $object): mixed
{
$embeddedObject = $this->parent->getValue($object);
if ($embeddedObject === null) {
return null;
}
return $this->child->getValue($embeddedObject);
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->child->getUnderlyingReflector();
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use BackedEnum;
use ReflectionProperty;
use function array_map;
use function is_array;
use function reset;
/** @internal */
class EnumPropertyAccessor implements PropertyAccessor
{
/** @param class-string<BackedEnum> $enumType */
public function __construct(private PropertyAccessor $parent, private string $enumType)
{
}
public function setValue(object $object, mixed $value): void
{
if ($value !== null) {
$value = $this->toEnum($value);
}
$this->parent->setValue($object, $value);
}
public function getValue(object $object): mixed
{
$enum = $this->parent->getValue($object);
if ($enum === null) {
return null;
}
return $this->fromEnum($enum);
}
/**
* @param BackedEnum|BackedEnum[] $enum
*
* @return ($enum is BackedEnum ? (string|int) : (string[]|int[]))
*/
private function fromEnum($enum) // phpcs:ignore
{
if (is_array($enum)) {
return array_map(static function (BackedEnum $enum) {
return $enum->value;
}, $enum);
}
return $enum->value;
}
/**
* @phpstan-param BackedEnum|BackedEnum[]|int|string|int[]|string[] $value
*
* @return ($value is int|string|BackedEnum ? BackedEnum : BackedEnum[])
*/
private function toEnum($value): BackedEnum|array
{
if ($value instanceof BackedEnum) {
return $value;
}
if (is_array($value)) {
$v = reset($value);
if ($v instanceof BackedEnum) {
return $value;
}
return array_map([$this->enumType, 'from'], $value);
}
return $this->enumType::from($value);
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->parent->getUnderlyingReflector();
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use Doctrine\ORM\Proxy\InternalProxy;
use ReflectionProperty;
use function ltrim;
/** @internal */
class ObjectCastPropertyAccessor implements PropertyAccessor
{
/** @param class-string $class */
public static function fromNames(string $class, string $name): self
{
$reflectionProperty = new ReflectionProperty($class, $name);
$key = $reflectionProperty->isPrivate() ? "\0" . ltrim($class, '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name);
return new self($reflectionProperty, $key);
}
public static function fromReflectionProperty(ReflectionProperty $reflectionProperty): self
{
$name = $reflectionProperty->getName();
$key = $reflectionProperty->isPrivate() ? "\0" . ltrim($reflectionProperty->getDeclaringClass()->getName(), '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name);
return new self($reflectionProperty, $key);
}
private function __construct(private ReflectionProperty $reflectionProperty, private string $key)
{
}
public function setValue(object $object, mixed $value): void
{
if (! ($object instanceof InternalProxy && ! $object->__isInitialized())) {
$this->reflectionProperty->setValue($object, $value);
return;
}
$object->__setInitialized(true);
$this->reflectionProperty->setValue($object, $value);
$object->__setInitialized(false);
}
public function getValue(object $object): mixed
{
return ((array) $object)[$this->key] ?? null;
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->reflectionProperty;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use ReflectionProperty;
/**
* A property accessor is a class that allows to read and write properties on objects regardless of visibility.
*
* We use them while creating objects from database rows in {@link UnitOfWork::createEntity()} or when
* computing changesets from objects that are about to be written back to the database in {@link UnitOfWork::computeChangeSet()}.
*
* This abstraction over ReflectionProperty is necessary, because for several features of either Doctrine or PHP, we
* need to handle edge cases in reflection at a central location in the code.
*
* @internal
*/
interface PropertyAccessor
{
public function setValue(object $object, mixed $value): void;
public function getValue(object $object): mixed;
public function getUnderlyingReflector(): ReflectionProperty;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use ReflectionProperty;
use const PHP_VERSION_ID;
class PropertyAccessorFactory
{
/** @phpstan-param class-string $className */
public static function createPropertyAccessor(string $className, string $propertyName): PropertyAccessor
{
$reflectionProperty = new ReflectionProperty($className, $propertyName);
$accessor = PHP_VERSION_ID >= 80400
? RawValuePropertyAccessor::fromReflectionProperty($reflectionProperty)
: ObjectCastPropertyAccessor::fromReflectionProperty($reflectionProperty);
if ($reflectionProperty->hasType() && ! $reflectionProperty->getType()->allowsNull()) {
$accessor = new TypedNoDefaultPropertyAccessor($accessor, $reflectionProperty);
}
if ($reflectionProperty->isReadOnly()) {
$accessor = new ReadonlyAccessor($accessor, $reflectionProperty);
}
return $accessor;
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use Doctrine\ORM\Proxy\InternalProxy;
use LogicException;
use ReflectionProperty;
use function ltrim;
use const PHP_VERSION_ID;
/**
* This is a PHP 8.4 and up only class and replaces ObjectCastPropertyAccessor.
*
* It works based on the raw values of a property, which for a case of property hooks
* is the backed value. If we kept using setValue/getValue, this would go through the hooks,
* which potentially change the data.
*/
class RawValuePropertyAccessor implements PropertyAccessor
{
public static function fromReflectionProperty(ReflectionProperty $reflectionProperty): self
{
$name = $reflectionProperty->getName();
$key = $reflectionProperty->isPrivate() ? "\0" . ltrim($reflectionProperty->getDeclaringClass()->getName(), '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name);
return new self($reflectionProperty, $key);
}
private function __construct(private ReflectionProperty $reflectionProperty, private string $key)
{
if (PHP_VERSION_ID < 80400) {
throw new LogicException('This class requires PHP 8.4 or higher.');
}
}
public function setValue(object $object, mixed $value): void
{
if (! ($object instanceof InternalProxy && ! $object->__isInitialized())) {
$this->reflectionProperty->setRawValueWithoutLazyInitialization($object, $value);
return;
}
$object->__setInitialized(true);
$this->reflectionProperty->setRawValue($object, $value);
$object->__setInitialized(false);
}
public function getValue(object $object): mixed
{
return ((array) $object)[$this->key] ?? null;
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->reflectionProperty;
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use InvalidArgumentException;
use LogicException;
use ReflectionProperty;
use function sprintf;
/** @internal */
class ReadonlyAccessor implements PropertyAccessor
{
public function __construct(private PropertyAccessor $parent, private ReflectionProperty $reflectionProperty)
{
if (! $this->reflectionProperty->isReadOnly()) {
throw new InvalidArgumentException(sprintf(
'%s::$%s must be readonly property',
$this->reflectionProperty->getDeclaringClass()->getName(),
$this->reflectionProperty->getName(),
));
}
}
public function setValue(object $object, mixed $value): void
{
if (! $this->reflectionProperty->isInitialized($object)) {
$this->parent->setValue($object, $value);
return;
}
if ($this->parent->getValue($object) !== $value) {
throw new LogicException(sprintf(
'Attempting to change readonly property %s::$%s.',
$this->reflectionProperty->getDeclaringClass()->getName(),
$this->reflectionProperty->getName(),
));
}
}
public function getValue(object $object): mixed
{
return $this->parent->getValue($object);
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->reflectionProperty;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use Closure;
use InvalidArgumentException;
use ReflectionProperty;
use function assert;
use function sprintf;
/** @internal */
class TypedNoDefaultPropertyAccessor implements PropertyAccessor
{
private Closure|null $unsetter = null;
public function __construct(private PropertyAccessor $parent, private ReflectionProperty $reflectionProperty)
{
if (! $this->reflectionProperty->hasType()) {
throw new InvalidArgumentException(sprintf(
'%s::$%s must have a type when used with TypedNoDefaultPropertyAccessor',
$this->reflectionProperty->getDeclaringClass()->getName(),
$this->reflectionProperty->getName(),
));
}
if ($this->reflectionProperty->getType()->allowsNull()) {
throw new InvalidArgumentException(sprintf(
'%s::$%s must not be nullable when used with TypedNoDefaultPropertyAccessor',
$this->reflectionProperty->getDeclaringClass()->getName(),
$this->reflectionProperty->getName(),
));
}
}
public function setValue(object $object, mixed $value): void
{
if ($value === null) {
if ($this->unsetter === null) {
$propertyName = $this->reflectionProperty->getName();
$this->unsetter = function () use ($propertyName): void {
unset($this->$propertyName);
};
}
$unsetter = $this->unsetter->bindTo($object, $this->reflectionProperty->getDeclaringClass()->getName());
assert($unsetter instanceof Closure);
$unsetter();
return;
}
$this->parent->setValue($object, $value);
}
public function getValue(object $object): mixed
{
return $this->reflectionProperty->isInitialized($object) ? $this->parent->getValue($object) : null;
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->reflectionProperty;
}
}

View File

@@ -107,6 +107,10 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
if (empty($joinColumn['name'])) {
$mappingArray['joinColumns'][$index]['name'] = $namingStrategy->joinColumnName($mappingArray['fieldName'], $name);
}
if (empty($joinColumn['referencedColumnName'])) {
$mappingArray['joinColumns'][$index]['referencedColumnName'] = $namingStrategy->referenceColumnName();
}
}
}

View File

@@ -140,7 +140,7 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) {
assert($this->typeClass !== null);
// Set back reference to owner
$this->typeClass->reflFields[$this->backRefFieldName]->setValue(
$this->typeClass->propertyAccessors[$this->backRefFieldName]->setValue(
$element,
$this->owner,
);
@@ -166,7 +166,7 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) {
assert($this->typeClass !== null);
// Set back reference to owner
$this->typeClass->reflFields[$this->backRefFieldName]->setValue(
$this->typeClass->propertyAccessors[$this->backRefFieldName]->setValue(
$element,
$this->owner,
);

View File

@@ -201,6 +201,16 @@ class BasicEntityPersister implements EntityPersister
);
}
final protected function isFilterHashUpToDate(): bool
{
return $this->filterHash === $this->em->getFilters()->getHash();
}
final protected function updateFilterHash(): void
{
$this->filterHash = $this->em->getFilters()->getHash();
}
public function getClassMetadata(): ClassMetadata
{
return $this->class;
@@ -470,7 +480,7 @@ class BasicEntityPersister implements EntityPersister
$where[] = $versionColumn;
$types[] = $this->class->fieldMappings[$versionField]->type;
$params[] = $this->class->reflFields[$versionField]->getValue($entity);
$params[] = $this->class->propertyAccessors[$versionField]->getValue($entity);
switch ($versionFieldType) {
case Types::SMALLINT:
@@ -781,7 +791,7 @@ class BasicEntityPersister implements EntityPersister
// Complete bidirectional association, if necessary
if ($targetEntity !== null && $isInverseSingleValued) {
$targetClass->reflFields[$assoc->inversedBy]->setValue($targetEntity, $sourceEntity);
$targetClass->propertyAccessors[$assoc->inversedBy]->setValue($targetEntity, $sourceEntity);
}
return $targetEntity;
@@ -828,7 +838,7 @@ class BasicEntityPersister implements EntityPersister
}
} else {
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
$sourceClass->propertyAccessors[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
}
}
@@ -1045,7 +1055,7 @@ class BasicEntityPersister implements EntityPersister
switch (true) {
case $sourceClass->containsForeignIdentifier:
$field = $sourceClass->getFieldForColumn($sourceKeyColumn);
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
$value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity);
if (isset($sourceClass->associationMappings[$field])) {
$value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
@@ -1056,7 +1066,7 @@ class BasicEntityPersister implements EntityPersister
case isset($sourceClass->fieldNames[$sourceKeyColumn]):
$field = $sourceClass->fieldNames[$sourceKeyColumn];
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
$value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity);
break;
@@ -1231,7 +1241,7 @@ class BasicEntityPersister implements EntityPersister
*/
protected function getSelectColumnsSQL(): string
{
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->filterHash === $this->em->getFilters()->getHash()) {
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -1347,7 +1357,7 @@ class BasicEntityPersister implements EntityPersister
}
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
$this->filterHash = $this->em->getFilters()->getHash();
$this->updateFilterHash();
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -1460,7 +1470,7 @@ class BasicEntityPersister implements EntityPersister
{
$columns = [];
foreach ($this->class->reflFields as $name => $field) {
foreach ($this->class->propertyAccessors as $name => $field) {
if ($this->class->isVersioned && $this->class->versionField === $name) {
continue;
}
@@ -1814,7 +1824,7 @@ class BasicEntityPersister implements EntityPersister
foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) {
if ($sourceClass->containsForeignIdentifier) {
$field = $sourceClass->getFieldForColumn($sourceKeyColumn);
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
$value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity);
if (isset($sourceClass->associationMappings[$field])) {
$value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
@@ -1832,7 +1842,7 @@ class BasicEntityPersister implements EntityPersister
}
$field = $sourceClass->fieldNames[$sourceKeyColumn];
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
$value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity);
$criteria[$tableAlias . '.' . $targetKeyColumn] = $value;
$parameters[] = [

View File

@@ -358,7 +358,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
protected function getSelectColumnsSQL(): string
{
// Create the column list fragment only once
if ($this->currentPersisterContext->selectColumnListSql !== null) {
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -445,6 +445,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
}
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
$this->updateFilterHash();
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -459,7 +460,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
? $this->class->getIdentifierColumnNames()
: [];
foreach ($this->class->reflFields as $name => $field) {
foreach ($this->class->propertyAccessors as $name => $field) {
if (
isset($this->class->fieldMappings[$name]->inherited)
&& ! isset($this->class->fieldMappings[$name]->id)

View File

@@ -35,7 +35,7 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
protected function getSelectColumnsSQL(): string
{
$columnList = [];
if ($this->currentPersisterContext->selectColumnListSql !== null) {
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -89,6 +89,7 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
}
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
$this->updateFilterHash();
return $this->currentPersisterContext->selectColumnListSql;
}

View File

@@ -13,16 +13,19 @@ use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Proxy;
use LogicException;
use ReflectionClass;
use ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;
use function array_combine;
use function array_flip;
use function array_intersect_key;
use function array_keys;
use function assert;
use function bin2hex;
use function chmod;
use function class_exists;
use function count;
use function dirname;
use function file_exists;
use function file_put_contents;
@@ -37,6 +40,7 @@ use function preg_match_all;
use function random_bytes;
use function rename;
use function rtrim;
use function sprintf;
use function str_replace;
use function strpos;
use function strrpos;
@@ -45,6 +49,7 @@ use function substr;
use function ucfirst;
use const DIRECTORY_SEPARATOR;
use const PHP_VERSION_ID;
/**
* This factory is used to create proxy objects for entities at runtime.
@@ -127,6 +132,9 @@ EOPHP;
/** @var array<class-string, Closure> */
private array $proxyFactories = [];
private readonly string $proxyDir;
private readonly string $proxyNs;
/**
* Initializes a new instance of the <tt>ProxyFactory</tt> class that is
* connected to the given <tt>EntityManager</tt>.
@@ -138,15 +146,15 @@ EOPHP;
*/
public function __construct(
private readonly EntityManagerInterface $em,
private readonly string $proxyDir,
private readonly string $proxyNs,
string|null $proxyDir = null,
string|null $proxyNs = null,
bool|int $autoGenerate = self::AUTOGENERATE_NEVER,
) {
if (! $proxyDir) {
if (! $proxyDir && ! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
throw ORMInvalidArgumentException::proxyDirectoryRequired();
}
if (! $proxyNs) {
if (! $proxyNs && ! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
throw ORMInvalidArgumentException::proxyNamespaceRequired();
}
@@ -154,6 +162,17 @@ EOPHP;
throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate);
}
if ($proxyDir === null && $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$proxyDir = '';
}
if ($proxyNs === null && $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$proxyNs = '';
}
$this->proxyDir = $proxyDir;
$this->proxyNs = $proxyNs;
$this->uow = $em->getUnitOfWork();
$this->autoGenerate = (int) $autoGenerate;
$this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
@@ -163,8 +182,35 @@ EOPHP;
* @param class-string $className
* @param array<mixed> $identifier
*/
public function getProxy(string $className, array $identifier): InternalProxy
public function getProxy(string $className, array $identifier): object
{
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$classMetadata = $this->em->getClassMetadata($className);
$entityPersister = $this->uow->getEntityPersister($className);
$identifierFlattener = $this->identifierFlattener;
$proxy = $classMetadata->reflClass->newLazyGhost(static function (object $object) use (
$identifier,
$entityPersister,
$identifierFlattener,
$classMetadata,
): void {
$original = $entityPersister->loadById($identifier, $object);
if ($original === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
$classMetadata->getName(),
$identifierFlattener->flattenIdentifier($classMetadata, $identifier),
);
}
}, ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE);
foreach ($identifier as $idField => $value) {
$classMetadata->propertyAccessors[$idField]->setValue($proxy, $value);
}
return $proxy;
}
$proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className);
return $proxyFactory($identifier);
@@ -182,6 +228,10 @@ EOPHP;
*/
public function generateProxyClasses(array $classes, string|null $proxyDir = null): int
{
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
return 0;
}
$generated = 0;
foreach ($classes as $class) {
@@ -232,8 +282,8 @@ EOPHP;
$class = $entityPersister->getClassMetadata();
foreach ($class->getReflectionProperties() as $property) {
if (! $property || isset($identifier[$property->getName()])) {
foreach ($class->getPropertyAccessors() as $name => $property) {
if (isset($identifier[$name])) {
continue;
}
@@ -262,6 +312,14 @@ EOPHP;
foreach ($reflector->getProperties($filter) as $property) {
$name = $property->name;
if (PHP_VERSION_ID >= 80400 && count($property->getHooks()) > 0) {
throw new LogicException(sprintf(
'Doctrine ORM does not support property hook on %s::%s without using native lazy objects. Check https://github.com/doctrine/orm/issues/11624 for details of versions that support property hooks.',
$property->getDeclaringClass()->getName(),
$property->getName(),
));
}
if ($property->isStatic() || ! isset($identifiers[$name])) {
continue;
}
@@ -279,7 +337,11 @@ EOPHP;
$entityPersister = $this->uow->getEntityPersister($className);
$initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener);
$proxyClassName = $this->loadProxyClass($class);
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
$identifierFields = [];
foreach (array_keys($identifiers) as $identifier) {
$identifierFields[$identifier] = $class->getPropertyAccessor($identifier);
}
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
@@ -383,6 +445,7 @@ EOPHP;
private function generateUseLazyGhostTrait(ClassMetadata $class): string
{
// @phpstan-ignore staticMethod.deprecated (Because we support Symfony < 7.3)
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
$code = substr($code, 7 + (int) strpos($code, "\n{"));
$code = substr($code, 0, (int) strpos($code, "\n}"));

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Query\AST;
use Doctrine\ORM\Query\SqlWalker;
/**
* EntityAsDtoArgumentExpression ::= IdentificationVariable
*
* @link www.doctrine-project.org
*/
class EntityAsDtoArgumentExpression extends Node
{
public function __construct(
public mixed $expression,
public string|null $identificationVariable,
) {
}
public function dispatch(SqlWalker $walker): string
{
return $walker->walkEntityAsDtoArgumentExpression($this);
}
}

View File

@@ -1106,6 +1106,50 @@ final class Parser
return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
}
/**
* EntityAsDtoArgumentExpression ::= IdentificationVariable
*/
public function EntityAsDtoArgumentExpression(): AST\EntityAsDtoArgumentExpression
{
assert($this->lexer->lookahead !== null);
$expression = null;
$identVariable = null;
$peek = $this->lexer->glimpse();
$lookaheadType = $this->lexer->lookahead->type;
assert($peek !== null);
assert($lookaheadType === TokenType::T_IDENTIFIER);
assert($peek->type !== TokenType::T_DOT);
assert($peek->type !== TokenType::T_OPEN_PARENTHESIS);
$expression = $identVariable = $this->IdentificationVariable();
// [["AS"] AliasResultVariable]
$mustHaveAliasResultVariable = false;
if ($this->lexer->isNextToken(TokenType::T_AS)) {
$this->match(TokenType::T_AS);
$mustHaveAliasResultVariable = true;
}
$aliasResultVariable = null;
if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {
$token = $this->lexer->lookahead;
$aliasResultVariable = $this->AliasResultVariable();
// Include AliasResultVariable in query components.
$this->queryComponents[$aliasResultVariable] = [
'resultVariable' => $expression,
'nestingLevel' => $this->nestingLevel,
'token' => $token,
];
}
return new AST\EntityAsDtoArgumentExpression($expression, $identVariable);
}
/**
* SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
*/
@@ -1849,6 +1893,8 @@ final class Parser
$this->match(TokenType::T_CLOSE_PARENTHESIS);
} elseif ($token->type === TokenType::T_NEW) {
$expression = $this->NewObjectExpression();
} elseif ($token->type === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_DOT && $peek->type !== TokenType::T_OPEN_PARENTHESIS) {
$expression = $this->EntityAsDtoArgumentExpression();
} else {
$expression = $this->ScalarExpression();
}

View File

@@ -169,7 +169,7 @@ class ResultSetMapping
/**
* Maps last argument for new objects in order to initiate object construction
*
* @phpstan-var array<int|string, array{ownerIndex: string|int, argIndex: int|string}>
* @phpstan-var array<int|string, array{ownerIndex: string|int, argIndex: int|string, argAlias: string}>
*/
public array $nestedNewObjectArguments = [];
@@ -187,6 +187,13 @@ class ResultSetMapping
*/
public array $discriminatorParameters = [];
/**
* Entities nested in Dto's
*
* @phpstan-var array<string, array<string, (int|string)>>
*/
public array $nestedEntities = [];
/**
* Adds an entity result to this ResultSetMapping.
*

View File

@@ -575,6 +575,14 @@ class SqlWalker
return implode(', ', $sqlParts);
}
/**
* Walks down an EntityAsDtoArgumentExpression AST node, thereby generating the appropriate SQL.
*/
public function walkEntityAsDtoArgumentExpression(AST\EntityAsDtoArgumentExpression $expr): string
{
return implode(', ', $this->walkObjectExpression($expr->expression, [], $expr->identificationVariable ?: null));
}
/**
* Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL.
*/
@@ -1356,32 +1364,79 @@ class SqlWalker
$partialFieldSet = [];
}
$class = $this->getMetadataForDqlAlias($dqlAlias);
$resultAlias = $selectExpression->fieldIdentificationVariable ?: null;
$sql .= implode(', ', $this->walkObjectExpression($dqlAlias, $partialFieldSet, $selectExpression->fieldIdentificationVariable ?: null));
}
if (! isset($this->selectedClasses[$dqlAlias])) {
$this->selectedClasses[$dqlAlias] = [
'class' => $class,
'dqlAlias' => $dqlAlias,
'resultAlias' => $resultAlias,
];
}
return $sql;
}
$sqlParts = [];
/**
* Walks down an Object Expression AST node and return Sql Parts
*
* @param mixed[] $partialFieldSet
*
* @return string[]
*/
public function walkObjectExpression(string $dqlAlias, array $partialFieldSet, string|null $resultAlias): array
{
$class = $this->getMetadataForDqlAlias($dqlAlias);
// Select all fields from the queried class
foreach ($class->fieldMappings as $fieldName => $mapping) {
if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true)) {
if (! isset($this->selectedClasses[$dqlAlias])) {
$this->selectedClasses[$dqlAlias] = [
'class' => $class,
'dqlAlias' => $dqlAlias,
'resultAlias' => $resultAlias,
];
}
$sqlParts = [];
// Select all fields from the queried class
foreach ($class->fieldMappings as $fieldName => $mapping) {
if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true)) {
continue;
}
$tableName = isset($mapping->inherited)
? $this->em->getClassMetadata($mapping->inherited)->getTableName()
: $class->getTableName();
$sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
$col = $sqlTableAlias . '.' . $quotedColumnName;
$type = Type::getType($mapping->type);
$col = $type->convertToPHPValueSQL($col, $this->platform);
$sqlParts[] = $col . ' AS ' . $columnAlias;
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);
if (! empty($mapping->enumType)) {
$this->rsm->addEnumResult($columnAlias, $mapping->enumType);
}
}
// Add any additional fields of subclasses (excluding inherited fields)
// 1) on Single Table Inheritance: always, since its marginal overhead
// 2) on Class Table Inheritance only if partial objects are disallowed,
// since it requires outer joining subtables.
if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
foreach ($class->subClasses as $subClassName) {
$subClass = $this->em->getClassMetadata($subClassName);
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
continue;
}
$tableName = isset($mapping->inherited)
? $this->em->getClassMetadata($mapping->inherited)->getTableName()
: $class->getTableName();
$sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
$col = $sqlTableAlias . '.' . $quotedColumnName;
@@ -1392,48 +1447,12 @@ class SqlWalker
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);
if (! empty($mapping->enumType)) {
$this->rsm->addEnumResult($columnAlias, $mapping->enumType);
}
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
}
// Add any additional fields of subclasses (excluding inherited fields)
// 1) on Single Table Inheritance: always, since its marginal overhead
// 2) on Class Table Inheritance only if partial objects are disallowed,
// since it requires outer joining subtables.
if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
foreach ($class->subClasses as $subClassName) {
$subClass = $this->em->getClassMetadata($subClassName);
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
continue;
}
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
$col = $sqlTableAlias . '.' . $quotedColumnName;
$type = Type::getType($mapping->type);
$col = $type->convertToPHPValueSQL($col, $this->platform);
$sqlParts[] = $col . ' AS ' . $columnAlias;
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
}
}
}
$sql .= implode(', ', $sqlParts);
}
}
return $sql;
return $sqlParts;
}
public function walkQuantifiedExpression(AST\QuantifiedExpression $qExpr): string
@@ -1549,6 +1568,14 @@ class SqlWalker
$sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
break;
case $e instanceof AST\EntityAsDtoArgumentExpression:
$alias = $e->identificationVariable ?: $columnAlias;
$this->rsm->nestedNewObjectArguments[$columnAlias] = ['ownerIndex' => $objIndex, 'argIndex' => $argIndex, 'argAlias' => $alias];
$this->rsm->nestedEntities[$alias] = ['parent' => $objIndex, 'argIndex' => $argIndex, 'type' => 'entity'];
$sqlSelectExpressions[] = trim($e->dispatch($this));
break;
default:
$sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
break;

View File

@@ -9,7 +9,6 @@ use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Doctrine\ORM\Internal\NoUnknownNamedArguments;
use Doctrine\ORM\Internal\QueryType;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\QueryExpressionVisitor;
@@ -128,6 +127,11 @@ class QueryBuilder implements Stringable
$this->parameters = new ArrayCollection();
}
final protected function getType(): QueryType
{
return $this->type;
}
/**
* Gets an ExpressionBuilder used for object-oriented construction of query expressions.
* This producer method is intended for convenient inline usage. Example:

View File

@@ -2,9 +2,8 @@
declare(strict_types=1);
namespace Doctrine\ORM\Internal;
namespace Doctrine\ORM;
/** @internal To be used inside the QueryBuilder only. */
enum QueryType
{
case Select;

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use function method_exists;
/**
* Forward compatibility with Symfony Console 7.4
*
* @internal
*/
trait ApplicationCompatibility
{
private static function addCommandToApplication(Application $application, Command $command): Command|null
{
if (method_exists(Application::class, 'addCommand')) {
// @phpstan-ignore method.notFound (This method will be added in Symfony 7.4)
return $application->addCommand($command);
}
return $application->add($command);
}
}

View File

@@ -34,7 +34,7 @@ EOT);
protected function execute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui = new SymfonyStyle($input, $output);
$entityManager = $this->getEntityManager($input);
@@ -43,7 +43,7 @@ EOT);
->getAllClassNames();
if (! $entityClassNames) {
$ui->caution(
$ui->getErrorStyle()->caution(
[
'You do not have any mapped Doctrine ORM entities according to the current configuration.',
'If you have entities or mapping files you should check your mapping configuration for errors.',

View File

@@ -64,7 +64,7 @@ EOT);
protected function execute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui = new SymfonyStyle($input, $output);
$entityManager = $this->getEntityManager($input);

View File

@@ -19,6 +19,8 @@ use function class_exists;
*/
final class ConsoleRunner
{
use ApplicationCompatibility;
/**
* Runs console with the given helper set.
*
@@ -59,7 +61,10 @@ final class ConsoleRunner
$connectionProvider = new ConnectionFromManagerProvider($entityManagerProvider);
if (class_exists(DBALConsole\Command\ReservedWordsCommand::class)) {
$cli->add(new DBALConsole\Command\ReservedWordsCommand($connectionProvider));
self::addCommandToApplication(
$cli,
new DBALConsole\Command\ReservedWordsCommand($connectionProvider),
);
}
$cli->addCommands(

View File

@@ -718,6 +718,10 @@ class SchemaTool
if (isset($joinColumn->onDelete)) {
$fkOptions['onDelete'] = $joinColumn->onDelete;
}
if (isset($joinColumn->deferrable)) {
$fkOptions['deferrable'] = $joinColumn->deferrable;
}
}
// Prefer unique constraints over implicit simple indexes created for foreign keys.

View File

@@ -31,7 +31,6 @@ use function array_map;
use function array_push;
use function array_search;
use function array_values;
use function assert;
use function class_exists;
use function class_parents;
use function count;
@@ -329,9 +328,8 @@ class SchemaValidator
array_filter(
array_map(
function (FieldMapping $fieldMapping) use ($class): string|null {
$fieldName = $fieldMapping->fieldName;
assert(isset($class->reflFields[$fieldName]));
$propertyType = $class->reflFields[$fieldName]->getType();
$fieldName = $fieldMapping->fieldName;
$propertyType = $class->propertyAccessors[$fieldName]->getUnderlyingReflector()->getType();
// If the field type is not a built-in type, we cannot check it
if (! Type::hasType($fieldMapping->type)) {

View File

@@ -586,7 +586,7 @@ class UnitOfWork implements PropertyChangedListener
$actualData = [];
foreach ($class->reflFields as $name => $refProp) {
foreach ($class->propertyAccessors as $name => $refProp) {
$value = $refProp->getValue($entity);
if ($class->isCollectionValuedAssociation($name) && $value !== null) {
@@ -706,7 +706,7 @@ class UnitOfWork implements PropertyChangedListener
$newValue = clone $actualValue;
$newValue->setOwner($entity, $assoc);
$class->reflFields[$propName]->setValue($entity, $newValue);
$class->propertyAccessors[$propName]->setValue($entity, $newValue);
}
}
@@ -745,7 +745,7 @@ class UnitOfWork implements PropertyChangedListener
// Look for changes in associations of the entity
foreach ($class->associationMappings as $field => $assoc) {
$val = $class->reflFields[$field]->getValue($entity);
$val = $class->propertyAccessors[$field]->getValue($entity);
if ($val === null) {
continue;
}
@@ -981,7 +981,7 @@ class UnitOfWork implements PropertyChangedListener
$actualData = [];
foreach ($class->reflFields as $name => $refProp) {
foreach ($class->propertyAccessors as $name => $refProp) {
if (
( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
&& ($name !== $class->versionField)
@@ -1167,7 +1167,7 @@ class UnitOfWork implements PropertyChangedListener
// is obtained by a new entity because the old one went out of scope.
//$this->entityStates[$oid] = self::STATE_NEW;
if (! $class->isIdentifierNatural()) {
$class->reflFields[$class->identifier[0]]->setValue($entity, null);
$class->propertyAccessors[$class->identifier[0]]->setValue($entity, null);
}
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
@@ -2029,7 +2029,7 @@ class UnitOfWork implements PropertyChangedListener
);
foreach ($associationMappings as $assoc) {
$relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
$relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity);
switch (true) {
case $relatedEntities instanceof PersistentCollection:
@@ -2070,7 +2070,7 @@ class UnitOfWork implements PropertyChangedListener
);
foreach ($associationMappings as $assoc) {
$relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
$relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity);
switch (true) {
case $relatedEntities instanceof PersistentCollection:
@@ -2116,7 +2116,7 @@ class UnitOfWork implements PropertyChangedListener
);
foreach ($associationMappings as $assoc) {
$relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
$relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity);
switch (true) {
case $relatedEntities instanceof PersistentCollection:
@@ -2179,7 +2179,7 @@ class UnitOfWork implements PropertyChangedListener
$entitiesToCascade = [];
foreach ($associationMappings as $assoc) {
$relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
$relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity);
switch (true) {
case $relatedEntities instanceof Collection:
@@ -2235,7 +2235,7 @@ class UnitOfWork implements PropertyChangedListener
$this->initializeObject($entity);
assert($class->versionField !== null);
$entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
$entityVersion = $class->propertyAccessors[$class->versionField]->getValue($entity);
// phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator
if ($entityVersion != $lockVersion) {
@@ -2379,7 +2379,11 @@ class UnitOfWork implements PropertyChangedListener
}
if ($this->isUninitializedObject($entity)) {
$entity->__setInitialized(true);
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$class->reflClass->markLazyObjectAsInitialized($entity);
} else {
$entity->__setInitialized(true);
}
Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
} else {
@@ -2404,7 +2408,7 @@ class UnitOfWork implements PropertyChangedListener
foreach ($data as $field => $value) {
if (isset($class->fieldMappings[$field])) {
$class->reflFields[$field]->setValue($entity, $value);
$class->propertyAccessors[$field]->setValue($entity, $value);
}
}
@@ -2434,21 +2438,21 @@ class UnitOfWork implements PropertyChangedListener
if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
$this->originalEntityData[$oid][$field] = $data[$field];
$class->reflFields[$field]->setValue($entity, $data[$field]);
$targetClass->reflFields[$assoc->mappedBy]->setValue($data[$field], $entity);
$class->propertyAccessors[$field]->setValue($entity, $data[$field]);
$targetClass->propertyAccessors[$assoc->mappedBy]->setValue($data[$field], $entity);
continue 2;
}
// Inverse side of x-to-one can never be lazy
$class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity));
$class->propertyAccessors[$field]->setValue($entity, $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity));
continue 2;
}
// use the entity association
if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
$class->reflFields[$field]->setValue($entity, $data[$field]);
$class->propertyAccessors[$field]->setValue($entity, $data[$field]);
$this->originalEntityData[$oid][$field] = $data[$field];
break;
@@ -2480,7 +2484,7 @@ class UnitOfWork implements PropertyChangedListener
if (! $associatedId) {
// Foreign key is NULL
$class->reflFields[$field]->setValue($entity, null);
$class->propertyAccessors[$field]->setValue($entity, null);
$this->originalEntityData[$oid][$field] = null;
break;
@@ -2546,11 +2550,11 @@ class UnitOfWork implements PropertyChangedListener
}
$this->originalEntityData[$oid][$field] = $newValue;
$class->reflFields[$field]->setValue($entity, $newValue);
$class->propertyAccessors[$field]->setValue($entity, $newValue);
if ($assoc->inversedBy !== null && $assoc->isOneToOne() && $newValue !== null) {
$inverseAssoc = $targetClass->associationMappings[$assoc->inversedBy];
$targetClass->reflFields[$inverseAssoc->fieldName]->setValue($newValue, $entity);
$targetClass->propertyAccessors[$inverseAssoc->fieldName]->setValue($newValue, $entity);
}
break;
@@ -2566,7 +2570,7 @@ class UnitOfWork implements PropertyChangedListener
if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
$data[$field]->setOwner($entity, $assoc);
$class->reflFields[$field]->setValue($entity, $data[$field]);
$class->propertyAccessors[$field]->setValue($entity, $data[$field]);
$this->originalEntityData[$oid][$field] = $data[$field];
break;
@@ -2577,7 +2581,7 @@ class UnitOfWork implements PropertyChangedListener
$pColl->setOwner($entity, $assoc);
$pColl->setInitialized(false);
$reflField = $class->reflFields[$field];
$reflField = $class->propertyAccessors[$field];
$reflField->setValue($entity, $pColl);
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
@@ -2657,7 +2661,7 @@ class UnitOfWork implements PropertyChangedListener
$found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping->orderBy);
$targetClass = $this->em->getClassMetadata($targetEntity);
$targetProperty = $targetClass->getReflectionProperty($mappedBy);
$targetProperty = $targetClass->getPropertyAccessor($mappedBy);
assert($targetProperty !== null);
foreach ($found as $targetValue) {
@@ -2679,7 +2683,7 @@ class UnitOfWork implements PropertyChangedListener
$idHash = implode(' ', $id);
if ($mapping->indexBy !== null) {
$indexByProperty = $targetClass->getReflectionProperty($mapping->indexBy);
$indexByProperty = $targetClass->getPropertyAccessor($mapping->indexBy);
assert($indexByProperty !== null);
$collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue);
} else {
@@ -3036,6 +3040,13 @@ class UnitOfWork implements PropertyChangedListener
if ($obj instanceof PersistentCollection) {
$obj->initialize();
return;
}
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$reflection = $this->em->getClassMetadata($obj::class)->getReflectionClass();
$reflection->initializeLazyObject($obj);
}
}
@@ -3046,6 +3057,10 @@ class UnitOfWork implements PropertyChangedListener
*/
public function isUninitializedObject(mixed $obj): bool
{
if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled() && ! ($obj instanceof Collection) && is_object($obj)) {
return $this->em->getClassMetadata($obj::class)->reflClass->isUninitializedLazyObject($obj);
}
return $obj instanceof InternalProxy && ! $obj->__isInitialized();
}
@@ -3244,7 +3259,7 @@ class UnitOfWork implements PropertyChangedListener
$idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $generatedId);
$oid = spl_object_id($entity);
$class->reflFields[$idField]->setValue($entity, $idValue);
$class->propertyAccessors[$idField]->setValue($entity, $idValue);
$this->entityIdentifiers[$oid] = [$idField => $idValue];
$this->entityStates[$oid] = self::STATE_MANAGED;

View File

@@ -0,0 +1,32 @@
<?php
// phpcs:ignoreFile
namespace Doctrine\Tests\Models\PropertyHooks;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Table;
#[Entity]
#[Table(name: 'property_hooks_user')]
class MappingVirtualProperty
{
#[Id, GeneratedValue, Column(type: Types::INTEGER)]
public ?int $id;
#[Column(type: Types::STRING)]
public string $first;
#[Column(type: Types::STRING)]
public string $last;
#[Column(type: Types::STRING)]
public string $fullName {
get => $this->first . " " . $this->last;
set {
[$this->first, $this->last] = explode(' ', $value, 2);
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
// phpcs:ignoreFile
namespace Doctrine\Tests\Models\PropertyHooks;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Table;
#[Entity]
#[Table(name: 'property_hooks_user')]
class User
{
#[Id, GeneratedValue, Column(type: Types::INTEGER)]
public ?int $id;
#[Column(type: Types::STRING)]
public string $first {
set {
if (strlen($value) === 0) {
throw new ValueError("Name must be non-empty");
}
$this->first = $value;
}
}
#[Column(type: Types::STRING)]
public string $last {
set {
if (strlen($value) === 0) {
throw new ValueError("Name must be non-empty");
}
$this->last = $value;
}
}
public string $fullName {
get => $this->first . " " . $this->last;
set {
[$this->first, $this->last] = explode(' ', $value, 2);
}
}
#[Column(type: Types::STRING)]
public string $language = 'de' {
// Override the "read" action with arbitrary logic.
get => strtoupper($this->language);
// Override the "write" action with arbitrary logic.
set {
$this->language = strtolower($value);
}
}
}

View File

@@ -25,9 +25,10 @@ use Generator;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RequiresPhp;
use ReflectionClass;
use ReflectionProperty;
use stdClass;
use Symfony\Component\VarExporter\LazyGhostTrait;
use TypeError;
class EntityManagerTest extends OrmTestCase
@@ -180,17 +181,12 @@ class EntityManagerTest extends OrmTestCase
}
/** Resetting the EntityManager relies on lazy objects until https://github.com/doctrine/orm/issues/5933 is resolved */
#[RequiresPhp('8.4')]
public function testLazyGhostEntityManager(): void
{
$em = new class () extends EntityManager {
use LazyGhostTrait;
$reflector = new ReflectionClass(EntityManager::class);
public function __construct()
{
}
};
$em = $em::createLazyGhost(static function ($em): void {
$em = $reflector->newLazyGhost($initializer = static function (EntityManager $em): void {
$r = new ReflectionProperty(EntityManager::class, 'unitOfWork');
$r->setValue($em, new class () extends UnitOfWork {
public function __construct()
@@ -207,7 +203,7 @@ class EntityManagerTest extends OrmTestCase
$em->close();
$this->assertFalse($em->isOpen());
$em->resetLazyObject();
$reflector->resetAsLazyGhost($em, $initializer);
$this->assertTrue($em->isOpen());
}

View File

@@ -9,7 +9,6 @@ use Doctrine\ORM\Exception\EntityIdentityCollisionException;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\IterableTester;
@@ -557,7 +556,7 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
$this->_em->persist($article);
$this->_em->flush();
self::assertFalse($userRef->__isInitialized());
self::assertTrue($this->isUninitializedObject($userRef));
$this->_em->clear();
@@ -592,7 +591,7 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
$this->_em->persist($user);
$this->_em->flush();
self::assertFalse($groupRef->__isInitialized());
self::assertTrue($this->isUninitializedObject($groupRef));
$this->_em->clear();
@@ -940,8 +939,7 @@ class BasicFunctionalTest extends OrmFunctionalTestCase
->setParameter(1, $article->id)
->setFetchMode(CmsArticle::class, 'user', ClassMetadata::FETCH_EAGER)
->getSingleResult();
self::assertInstanceOf(InternalProxy::class, $article->user, 'It IS a proxy, ...');
self::assertFalse($this->isUninitializedObject($article->user), '...but its initialized!');
self::assertFalse($this->isUninitializedObject($article->user));
$this->assertQueryCount(2);
}

View File

@@ -470,7 +470,7 @@ EXCEPTION
public function testItAllowsReadingAttributes(): void
{
$metadata = $this->_em->getClassMetadata(Card::class);
$property = $metadata->getReflectionProperty('suit');
$property = $metadata->propertyAccessors['suit']->getUnderlyingReflector();
$attributes = $property->getAttributes();

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Mapping\Table;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* Functional tests for the Class Table Inheritance mapping strategy.
*/
class InvalidMappingDefinitionTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
}
public function testManyToManyRelationWithJoinTableOnTheWrongSide(): void
{
$this->expectException(MappingException::class);
$this->expectExceptionMessage("Mapping error on field 'owners' in Doctrine\Tests\ORM\Functional\OwnedSideEntity : 'joinTable' can only be set on many-to-many owning side.");
$this->createSchemaForModels(
OwningSideEntity::class,
OwnedSideEntity::class,
);
}
}
#[Table(name: 'owning_side_entities1')]
#[Entity]
class OwningSideEntity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\ManyToMany(targetEntity: OwnedSideEntity::class, inversedBy: 'owners')]
#[ORM\JoinTable(name: 'owning_owned')]
private Collection $relations;
public function __construct()
{
$this->relations = new ArrayCollection();
}
public function getId(): int
{
return $this->id;
}
public function getRelations(): Collection
{
return $this->relations;
}
public function addRelation(OwnedSideEntity $ownedSide): void
{
if (! $this->relations->contains($ownedSide)) {
$this->relations->add($ownedSide);
$ownedSide->addOwner($this);
}
}
public function removeRelation(OwnedSideEntity $ownedSide): void
{
if ($this->relations->removeElement($ownedSide)) {
$ownedSide->removeOwner($this);
}
}
}
#[Table(name: 'owned_side_entities1')]
#[Entity]
class OwnedSideEntity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 255)]
private string $data;
#[ORM\ManyToMany(targetEntity: OwningSideEntity::class, mappedBy: 'relations')]
#[ORM\JoinTable(name: 'owning_owned')]
private Collection $owners;
public function __construct()
{
$this->owners = new ArrayCollection();
}
public function getId(): int
{
return $this->id;
}
public function getData(): string
{
return $this->data;
}
public function setData(string $data): void
{
$this->data = $data;
}
public function getOwners(): Collection
{
return $this->owners;
}
public function addOwner(OwningSideEntity $owningSide): void
{
if (! $this->owners->contains($owningSide)) {
$this->owners->add($owningSide);
}
}
public function removeOwner(OwningSideEntity $owningSide): void
{
$this->owners->removeElement($owningSide);
}
}

View File

@@ -134,7 +134,7 @@ class LifecycleCallbackTest extends OrmFunctionalTestCase
LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation
$reference = $this->_em->getReference(LifecycleCallbackTestEntity::class, $id);
self::assertFalse($reference::$postLoadCallbackInvoked);
self::assertArrayNotHasKey('postLoadCallbackInvoked', (array) $reference);
$this->assertTrue($this->isUninitializedObject($reference));
$reference->getValue(); // trigger proxy load

View File

@@ -1230,6 +1230,170 @@ class NewOperatorTest extends OrmFunctionalTestCase
self::assertSame($this->fixtures[2]->address->zip, $result[2]->val2->zip);
}
public function testEntityInDtoWithRoot(): void
{
$dql = '
SELECT
new CmsDumbDTO(
u.id,
u,
a,
e.email
)
FROM
Doctrine\Tests\Models\CMS\CmsUser u
LEFT JOIN
u.email e
LEFT JOIN
u.address a
ORDER BY
u.name';
$query = $this->getEntityManager()->createQuery($dql);
$result = $query->getResult();
self::assertCount(3, $result);
self::assertInstanceOf(CmsDumbDTO::class, $result[0]);
self::assertInstanceOf(CmsDumbDTO::class, $result[1]);
self::assertInstanceOf(CmsDumbDTO::class, $result[2]);
self::assertInstanceOf(CmsUser::class, $result[0]->val2);
self::assertInstanceOf(CmsUser::class, $result[1]->val2);
self::assertInstanceOf(CmsUser::class, $result[2]->val2);
self::assertSame($this->fixtures[0]->name, $result[0]->val2->name);
self::assertSame($this->fixtures[1]->name, $result[1]->val2->name);
self::assertSame($this->fixtures[2]->name, $result[2]->val2->name);
self::assertSame($this->fixtures[0]->username, $result[0]->val2->username);
self::assertSame($this->fixtures[1]->username, $result[1]->val2->username);
self::assertSame($this->fixtures[2]->username, $result[2]->val2->username);
self::assertSame($this->fixtures[0]->status, $result[0]->val2->status);
self::assertSame($this->fixtures[1]->status, $result[1]->val2->status);
self::assertSame($this->fixtures[2]->status, $result[2]->val2->status);
self::assertInstanceOf(CmsAddress::class, $result[0]->val3);
self::assertInstanceOf(CmsAddress::class, $result[1]->val3);
self::assertInstanceOf(CmsAddress::class, $result[2]->val3);
self::assertSame($this->fixtures[0]->address->city, $result[0]->val3->city);
self::assertSame($this->fixtures[1]->address->city, $result[1]->val3->city);
self::assertSame($this->fixtures[2]->address->city, $result[2]->val3->city);
self::assertSame($this->fixtures[0]->address->country, $result[0]->val3->country);
self::assertSame($this->fixtures[1]->address->country, $result[1]->val3->country);
self::assertSame($this->fixtures[2]->address->country, $result[2]->val3->country);
self::assertSame($this->fixtures[0]->email->email, $result[0]->val4);
self::assertSame($this->fixtures[1]->email->email, $result[1]->val4);
self::assertSame($this->fixtures[2]->email->email, $result[2]->val4);
}
public function testEntityInDtoWithoutRoot(): void
{
$dql = '
SELECT
new CmsDumbDTO(
u.id,
u.name,
a,
e.email
)
FROM
Doctrine\Tests\Models\CMS\CmsUser u
LEFT JOIN
u.email e
LEFT JOIN
u.address a
ORDER BY
u.name';
$query = $this->getEntityManager()->createQuery($dql);
$result = $query->getResult();
self::assertCount(3, $result);
self::assertInstanceOf(CmsDumbDTO::class, $result[0]);
self::assertInstanceOf(CmsDumbDTO::class, $result[1]);
self::assertInstanceOf(CmsDumbDTO::class, $result[2]);
self::assertSame($this->fixtures[0]->name, $result[0]->val2);
self::assertSame($this->fixtures[1]->name, $result[1]->val2);
self::assertSame($this->fixtures[2]->name, $result[2]->val2);
self::assertInstanceOf(CmsAddress::class, $result[0]->val3);
self::assertInstanceOf(CmsAddress::class, $result[1]->val3);
self::assertInstanceOf(CmsAddress::class, $result[2]->val3);
self::assertSame($this->fixtures[0]->address->city, $result[0]->val3->city);
self::assertSame($this->fixtures[1]->address->city, $result[1]->val3->city);
self::assertSame($this->fixtures[2]->address->city, $result[2]->val3->city);
self::assertSame($this->fixtures[0]->address->country, $result[0]->val3->country);
self::assertSame($this->fixtures[1]->address->country, $result[1]->val3->country);
self::assertSame($this->fixtures[2]->address->country, $result[2]->val3->country);
self::assertSame($this->fixtures[0]->email->email, $result[0]->val4);
self::assertSame($this->fixtures[1]->email->email, $result[1]->val4);
self::assertSame($this->fixtures[2]->email->email, $result[2]->val4);
}
public function testOnlyObjectInDto(): void
{
$dql = '
SELECT
new CmsDumbDTO(
a,
new CmsDumbDTO(
u.name,
e.email
)
)
FROM
Doctrine\Tests\Models\CMS\CmsUser u
LEFT JOIN
u.email e
LEFT JOIN
u.address a
ORDER BY
u.name';
$query = $this->getEntityManager()->createQuery($dql);
$result = $query->getResult();
self::assertCount(3, $result);
self::assertInstanceOf(CmsDumbDTO::class, $result[0]);
self::assertInstanceOf(CmsDumbDTO::class, $result[1]);
self::assertInstanceOf(CmsDumbDTO::class, $result[2]);
self::assertInstanceOf(CmsAddress::class, $result[0]->val1);
self::assertInstanceOf(CmsAddress::class, $result[1]->val1);
self::assertInstanceOf(CmsAddress::class, $result[2]->val1);
self::assertSame($this->fixtures[0]->address->city, $result[0]->val1->city);
self::assertSame($this->fixtures[1]->address->city, $result[1]->val1->city);
self::assertSame($this->fixtures[2]->address->city, $result[2]->val1->city);
self::assertSame($this->fixtures[0]->address->country, $result[0]->val1->country);
self::assertSame($this->fixtures[1]->address->country, $result[1]->val1->country);
self::assertSame($this->fixtures[2]->address->country, $result[2]->val1->country);
self::assertInstanceOf(CmsDumbDTO::class, $result[0]->val2);
self::assertInstanceOf(CmsDumbDTO::class, $result[1]->val2);
self::assertInstanceOf(CmsDumbDTO::class, $result[2]->val2);
self::assertSame($this->fixtures[0]->name, $result[0]->val2->val1);
self::assertSame($this->fixtures[1]->name, $result[1]->val2->val1);
self::assertSame($this->fixtures[2]->name, $result[2]->val2->val1);
self::assertSame($this->fixtures[0]->email->email, $result[0]->val2->val2);
self::assertSame($this->fixtures[1]->email->email, $result[1]->val2->val2);
self::assertSame($this->fixtures[2]->email->email, $result[2]->val2->val2);
}
public function testNamedArguments(): void
{
$dql = <<<'SQL'

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\Tests\Models\PropertyHooks\MappingVirtualProperty;
use Doctrine\Tests\Models\PropertyHooks\User;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;
#[RequiresPhp('>= 8.4.0')]
class PropertyHooksTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
if ($this->_em->getConnection()->getDatabasePlatform() instanceof AbstractMySQLPlatform) {
self::markTestSkipped('MySQL/MariaDB is case-insensitive by default, and the logic of this test relies on case sensitivity.');
}
if (! $this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {
$this->markTestSkipped('Property hooks require native lazy objects to be enabled.');
}
$this->createSchemaForModels(
User::class,
);
}
public function testMapPropertyHooks(): void
{
$user = new User();
$user->fullName = 'John Doe';
$user->language = 'EN';
$this->_em->persist($user);
$this->_em->flush();
$this->_em->clear();
$user = $this->_em->find(User::class, $user->id);
self::assertEquals('John', $user->first);
self::assertEquals('Doe', $user->last);
self::assertEquals('John Doe', $user->fullName);
self::assertEquals('EN', $user->language, 'The property hook uppercases the language.');
$language = $this->_em->createQuery('SELECT u.language FROM ' . User::class . ' u WHERE u.id = :id')
->setParameter('id', $user->id)
->getSingleScalarResult();
$this->assertEquals('en', $language, 'Selecting a field from DQL does not go through the property hook, accessing raw data.');
$this->_em->clear();
$user = $this->_em->getRepository(User::class)->findOneBy(['language' => 'EN']);
self::assertNull($user);
$user = $this->_em->getRepository(User::class)->findOneBy(['language' => 'en']);
self::assertNotNull($user);
}
public function testTriggerLazyLoadingWhenAccessingPropertyHooks(): void
{
$user = new User();
$user->fullName = 'Ludwig von Beethoven';
$user->language = 'DE';
$this->_em->persist($user);
$this->_em->flush();
$this->_em->clear();
$user = $this->_em->getReference(User::class, $user->id);
$this->assertTrue($this->_em->getUnitOfWork()->isUninitializedObject($user));
self::assertEquals('Ludwig', $user->first);
self::assertEquals('von Beethoven', $user->last);
self::assertEquals('Ludwig von Beethoven', $user->fullName);
self::assertEquals('DE', $user->language, 'The property hook uppercases the language.');
$this->assertFalse($this->_em->getUnitOfWork()->isUninitializedObject($user));
$this->_em->clear();
$user = $this->_em->getReference(User::class, $user->id);
self::assertEquals('Ludwig von Beethoven', $user->fullName);
}
public function testMappingVirtualPropertyIsNotSupported(): void
{
$this->expectException(MappingException::class);
$this->expectExceptionMessage('Mapping virtual property "fullName" on entity "Doctrine\Tests\Models\PropertyHooks\MappingVirtualProperty" is not allowed.');
$this->_em->getClassMetadata(MappingVirtualProperty::class);
}
}

View File

@@ -34,6 +34,10 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
{
parent::setUp();
if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {
self::markTestSkipped('This test is not applicable when lazy proxy is enabled.');
}
$this->createSchemaForModels(
CmsUser::class,
CmsTag::class,

View File

@@ -6,7 +6,6 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Proxy\Proxy as CommonProxy;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\Tests\Models\Company\CompanyAuction;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct2;
@@ -242,13 +241,16 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
#[Group('DDC-1604')]
public function testCommonPersistenceProxy(): void
{
if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {
self::markTestSkipped('Test only works with proxy generation disabled.');
}
$id = $this->createProduct();
$entity = $this->_em->getReference(ECommerceProduct::class, $id);
assert($entity instanceof ECommerceProduct);
$className = DefaultProxyClassNameResolver::getClass($entity);
self::assertInstanceOf(InternalProxy::class, $entity);
self::assertTrue($this->isUninitializedObject($entity));
self::assertEquals(ECommerceProduct::class, $className);
@@ -257,7 +259,7 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
$proxyFileName = $this->_em->getConfiguration()->getProxyDir() . DIRECTORY_SEPARATOR . str_replace('\\', '', $restName) . '.php';
self::assertTrue(file_exists($proxyFileName), 'Proxy file name cannot be found generically.');
$entity->__load();
$this->initializeObject($entity);
self::assertFalse($this->isUninitializedObject($entity));
}
}

View File

@@ -16,6 +16,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function array_filter;
use function array_values;
use function implode;
use function str_starts_with;
@@ -42,6 +43,20 @@ class PostgreSqlSchemaToolTest extends OrmFunctionalTestCase
self::assertCount(0, $sql, implode("\n", $sql));
}
public function testSetDeferrableForeignKey(): void
{
$schema = $this->getSchemaForModels(
EntityWithSelfReferencingAssociation::class,
);
$table = $schema->getTable('entitywithselfreferencingassociation');
$fks = array_values($table->getForeignKeys());
self::assertCount(1, $fks);
self::assertTrue($fks[0]->getOption('deferrable'));
}
}
#[Table(name: 'stonewood.screen')]
@@ -98,3 +113,20 @@ class DDC1657Avatar
#[Column(name: 'pk', type: 'integer', nullable: false)]
private int $pk;
}
#[Table(name: 'entitywithselfreferencingassociation')]
#[Entity]
class EntityWithSelfReferencingAssociation
{
/**
* Identifier
*/
#[Id]
#[GeneratedValue(strategy: 'IDENTITY')]
#[Column(type: 'integer', nullable: false)]
private int $id;
#[ManyToOne(targetEntity: self::class)]
#[JoinColumn(deferrable: true)]
private self $parent;
}

View File

@@ -11,7 +11,6 @@ use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\Exception\CacheException;
use Doctrine\ORM\Cache\QueryCacheEntry;
use Doctrine\ORM\Cache\QueryCacheKey;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\Models\Cache\Attraction;
@@ -939,7 +938,6 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheFunctionalTestCase
self::assertNotNull($state1->getCountry());
$this->assertQueryCount(1);
self::assertInstanceOf(State::class, $state1);
self::assertInstanceOf(InternalProxy::class, $state1->getCountry());
self::assertEquals($countryName, $state1->getCountry()->getName());
self::assertEquals($stateId, $state1->getId());
@@ -957,7 +955,6 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheFunctionalTestCase
self::assertNotNull($state2->getCountry());
$this->assertQueryCount(0);
self::assertInstanceOf(State::class, $state2);
self::assertInstanceOf(InternalProxy::class, $state2->getCountry());
self::assertEquals($countryName, $state2->getCountry()->getName());
self::assertEquals($stateId, $state2->getId());
}
@@ -1031,7 +1028,6 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheFunctionalTestCase
$this->assertQueryCount(1);
self::assertInstanceOf(State::class, $state1);
self::assertInstanceOf(InternalProxy::class, $state1->getCountry());
self::assertInstanceOf(City::class, $state1->getCities()->get(0));
self::assertInstanceOf(State::class, $state1->getCities()->get(0)->getState());
self::assertSame($state1, $state1->getCities()->get(0)->getState());
@@ -1048,7 +1044,6 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheFunctionalTestCase
$this->assertQueryCount(0);
self::assertInstanceOf(State::class, $state2);
self::assertInstanceOf(InternalProxy::class, $state2->getCountry());
self::assertInstanceOf(City::class, $state2->getCities()->get(0));
self::assertInstanceOf(State::class, $state2->getCities()->get(0)->getState());
self::assertSame($state2, $state2->getCities()->get(0)->getState());

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\Tests\Models\Cache\Country;
use Doctrine\Tests\Models\Cache\State;
use PHPUnit\Framework\Attributes\Group;
@@ -198,8 +197,6 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheFunctionalTestCase
self::assertInstanceOf(State::class, $entities[1]);
self::assertInstanceOf(Country::class, $entities[0]->getCountry());
self::assertInstanceOf(Country::class, $entities[0]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry());
// load from cache
$this->getQueryLog()->reset()->enable();
@@ -212,8 +209,6 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheFunctionalTestCase
self::assertInstanceOf(State::class, $entities[1]);
self::assertInstanceOf(Country::class, $entities[0]->getCountry());
self::assertInstanceOf(Country::class, $entities[1]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry());
// invalidate cache
$this->_em->persist(new State('foo', $this->_em->find(Country::class, $this->countries[0]->getId())));
@@ -231,8 +226,6 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheFunctionalTestCase
self::assertInstanceOf(State::class, $entities[1]);
self::assertInstanceOf(Country::class, $entities[0]->getCountry());
self::assertInstanceOf(Country::class, $entities[1]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry());
// load from cache
$this->getQueryLog()->reset()->enable();
@@ -245,7 +238,5 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheFunctionalTestCase
self::assertInstanceOf(State::class, $entities[1]);
self::assertInstanceOf(Country::class, $entities[0]->getCountry());
self::assertInstanceOf(Country::class, $entities[1]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry());
self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry());
}
}

View File

@@ -57,11 +57,11 @@ class DDC1238Test extends OrmFunctionalTestCase
$user2 = $this->_em->getReference(DDC1238User::class, $userId);
//$user->__load();
//$this->initializeObject($user);
self::assertIsInt($user->getId(), 'Even if a proxy is detached, it should still have an identifier');
$user2->__load();
$this->initializeObject($user2);
self::assertIsInt($user2->getId(), 'The managed instance still has an identifier');
}

View File

@@ -25,7 +25,7 @@ class DDC168Test extends OrmFunctionalTestCase
$this->oldMetadata = $this->_em->getClassMetadata(CompanyEmployee::class);
$metadata = clone $this->oldMetadata;
ksort($metadata->reflFields);
ksort($metadata->propertyAccessors);
$this->_em->getMetadataFactory()->setMetadataFor(CompanyEmployee::class, $metadata);
}

View File

@@ -32,6 +32,10 @@ class GH10808Test extends OrmFunctionalTestCase
public function testDQLDeferredEagerLoad(): void
{
if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {
self::markTestSkipped('Test requires lazy loading to be disabled');
}
$appointment = new GH10808Appointment();
$this->_em->persist($appointment);

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
use Doctrine\Tests\OrmFunctionalTestCase;
use function sprintf;
use function str_replace;
abstract class AbstractTestCase extends OrmFunctionalTestCase
{
protected function generateMessage(string $message): string
{
$log = $this->getLastLoggedQuery();
return sprintf("%s\nSQL: %s", $message, str_replace(['?'], (array) $log['params'], $log['sql']));
}
protected function clearCachedData(object ...$entities): void
{
foreach ($entities as $entity) {
$this->_em->refresh($entity);
}
}
protected function persistFlushClear(object ...$entities): void
{
foreach ($entities as $entity) {
$this->_em->persist($entity);
}
$this->_em->flush();
$this->_em->clear();
}
}

View File

@@ -4,12 +4,11 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
use Doctrine\Tests\OrmFunctionalTestCase;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Order;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\User;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter\CompanySQLFilter;
use function sprintf;
use function str_replace;
final class ChangeFiltersTest extends OrmFunctionalTestCase
final class ChangeFiltersTest extends AbstractTestCase
{
private const COMPANY_A = 'A';
private const COMPANY_B = 'B';
@@ -130,11 +129,4 @@ final class ChangeFiltersTest extends OrmFunctionalTestCase
self::assertInstanceOf(User::class, $order->user);
self::assertEquals($companyB['userId'], $order->user->id);
}
private function generateMessage(string $message): string
{
$log = $this->getLastLoggedQuery();
return sprintf("%s\nSQL: %s", $message, str_replace(['?'], (array) $log['params'], $log['sql']));
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Insurance
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\Column(type: 'string')]
public string $name;
#[ORM\ManyToOne(targetEntity: Practice::class)]
public Practice $practice;
public function __construct(Practice $practice, string $name)
{
$this->practice = $practice;
$this->name = $name;
}
}

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Patient
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\Column(type: 'string')]
public string $name;
/** @var Collection<int, PatientInsurance> */
#[ORM\OneToMany(targetEntity: PatientInsurance::class, mappedBy: 'patient', fetch: 'LAZY', cascade: ['persist'])]
public Collection $insurances;
public function __construct(string $name)
{
$this->name = $name;
$this->insurances = new ArrayCollection();
}
/** @return Collection<PrimaryPatInsurance> */
public function getPrimaryInsurances(): Collection
{
return $this->insurances->filter(static function (PatientInsurance $insurances) {
return $insurances instanceof PrimaryPatInsurance;
});
}
/** @return Collection<SecondaryPatInsurance> */
public function getSecondaryInsurances(): Collection
{
return $this->insurances->filter(static function (PatientInsurance $insurances) {
return $insurances instanceof SecondaryPatInsurance;
});
}
public function addPrimaryInsurance(Insurance $insurance): void
{
$this->insurances[] = new PrimaryPatInsurance($this, $insurance);
}
public function addSecondaryInsurance(Insurance $insurance): void
{
$this->insurances[] = new SecondaryPatInsurance($this, $insurance);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorMap(['primary' => PrimaryPatInsurance::class, 'secondary' => SecondaryPatInsurance::class])]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
abstract class PatientInsurance
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\ManyToOne(targetEntity: Insurance::class, fetch: 'EAGER', cascade: ['persist'])]
#[ORM\JoinColumn(referencedColumnName: 'id', nullable: false)]
public Insurance $insurance;
#[ORM\ManyToOne(targetEntity: Patient::class, inversedBy: 'insurances')]
public Patient $patient;
public function __construct(Patient $patient, Insurance $insurance)
{
$this->patient = $patient;
$this->insurance = $insurance;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Practice
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\Column(type: 'string')]
public string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class PrimaryPatInsurance extends PatientInsurance
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class SecondaryPatInsurance extends PatientInsurance
{
}

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;

View File

@@ -2,10 +2,12 @@
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Order;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\User;
use function sprintf;

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Insurance;
use function sprintf;
class PracticeContextSQLFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if (! $this->hasParameter('practiceId') || $this->getParameter('practiceId') === null) {
return '';
}
if ($targetEntity->getName() === Insurance::class) {
return sprintf(
'%s.%s = %s',
$targetTableAlias,
$targetEntity->associationMappings['practice']->joinColumns[0]->name,
$this->getParameter('practiceId'),
);
}
return '';
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Insurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Patient;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\PatientInsurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Practice;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\PrimaryPatInsurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\SecondaryPatInsurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter\PracticeContextSQLFilter;
final class SwitchContextTest extends AbstractTestCase
{
/** @var SQLFilter|PracticeContextSQLFilter */
private $sqlFilter;
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
Practice::class,
Patient::class,
PatientInsurance::class,
PrimaryPatInsurance::class,
SecondaryPatInsurance::class,
Insurance::class,
);
$this->_em->getConfiguration()->addFilter(PracticeContextSQLFilter::class, PracticeContextSQLFilter::class);
$this->sqlFilter = $this->_em->getFilters()->enable(PracticeContextSQLFilter::class);
}
/** @return array{Patient, Patient} */
private function fixtureGenerate(): array
{
$practiceA = new Practice('Practice A');
$practiceB = new Practice('Practice B');
$insuranceAetna = new Insurance($practiceA, 'Aetna in Practice A');
$insuranceBHumana = new Insurance($practiceB, 'Humana in Practice B');
$insuranceBCustom = new Insurance($practiceB, 'Custom in Practice B');
$patientEgor = new Patient('Egor');
$patientEgor->addPrimaryInsurance($insuranceAetna);
$patientEgor->addPrimaryInsurance($insuranceBHumana);
$patientGena = new Patient('Gena');
$patientGena->addPrimaryInsurance($insuranceBHumana);
$patientGena->addSecondaryInsurance($insuranceBCustom);
$this->persistFlushClear(
$practiceA,
$practiceB,
$insuranceAetna,
$insuranceBHumana,
$insuranceBCustom,
$patientEgor,
$patientGena,
);
return [
$this->_em->getReference(Patient::class, $patientEgor->id),
$this->_em->getReference(Patient::class, $patientGena->id),
];
}
/**
* @param callable(): T $callback
*
* @return T
*
* @template T
*/
private function switchPracticeContext(Practice $practice, callable $callback)
{
$this->sqlFilter->setParameter('practiceId', $practice->id);
try {
return $callback();
} finally {
$this->sqlFilter->setParameter('practiceId', null);
}
}
public function testSwitchContext(): void
{
[$patientEgor, $patentGena] = $this->fixtureGenerate();
$practiceA = $this->_em->getRepository(Practice::class)->findOneBy(['name' => 'Practice A']);
$practiceB = $this->_em->getRepository(Practice::class)->findOneBy(['name' => 'Practice B']);
$this->switchPracticeContext($practiceA, function () use ($patientEgor, $patentGena): void {
$this->clearCachedData($patentGena, $patientEgor);
self::assertCount(1, $patientEgor->insurances);
self::assertInstanceOf(PrimaryPatInsurance::class, $patientEgor->getPrimaryInsurances()->first());
self::assertEquals('Aetna in Practice A', $patientEgor->getPrimaryInsurances()->first()->insurance->name);
self::assertCount(0, $patentGena->insurances);
});
$this->switchPracticeContext($practiceB, function () use ($patientEgor, $patentGena): void {
$this->clearCachedData($patentGena, $patientEgor);
self::assertCount(1, $patientEgor->insurances);
self::assertInstanceOf(PrimaryPatInsurance::class, $patientEgor->getPrimaryInsurances()->first());
self::assertEquals('Humana in Practice B', $patientEgor->getPrimaryInsurances()->first()->insurance->name);
self::assertCount(2, $patentGena->insurances);
self::assertInstanceOf(PrimaryPatInsurance::class, $patentGena->getPrimaryInsurances()->first());
self::assertInstanceOf(SecondaryPatInsurance::class, $patentGena->getSecondaryInsurances()->first());
self::assertEquals('Humana in Practice B', $patentGena->getPrimaryInsurances()->first()->insurance->name);
self::assertEquals('Custom in Practice B', $patentGena->getSecondaryInsurances()->first()->insurance->name);
});
}
}

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use DateTime;
use Doctrine\Common\Reflection\RuntimePublicReflectionProperty as CommonRuntimePublicReflectionProperty;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\DiscriminatorColumn;
use Doctrine\ORM\Mapping\DiscriminatorMap;
@@ -17,15 +16,11 @@ use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\InheritanceType;
use Doctrine\ORM\Mapping\MappedSuperclass;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Mapping\ReflectionEmbeddedProperty;
use Doctrine\ORM\Query\QueryException;
use Doctrine\Persistence\Reflection\RuntimeReflectionProperty;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use ReflectionProperty;
use function class_exists;
use function sprintf;
#[Group('DDC-93')]
@@ -45,30 +40,6 @@ class ValueObjectsTest extends OrmFunctionalTestCase
);
}
public function testMetadataHasReflectionEmbeddablesAccessible(): void
{
$classMetadata = $this->_em->getClassMetadata(DDC93Person::class);
if (class_exists(CommonRuntimePublicReflectionProperty::class)) {
self::assertInstanceOf(
CommonRuntimePublicReflectionProperty::class,
$classMetadata->getReflectionProperty('address'),
);
} elseif (class_exists(RuntimeReflectionProperty::class)) {
self::assertInstanceOf(
RuntimeReflectionProperty::class,
$classMetadata->getReflectionProperty('address'),
);
} else {
self::assertInstanceOf(
ReflectionProperty::class,
$classMetadata->getReflectionProperty('address'),
);
}
self::assertInstanceOf(ReflectionEmbeddedProperty::class, $classMetadata->getReflectionProperty('address.street'));
}
public function testCRUD(): void
{
$person = new DDC93Person();

View File

@@ -9,7 +9,6 @@ use Doctrine\ORM\Internal\Hydration\HydrationException;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\Mocks\ArrayResultFactory;
@@ -1030,7 +1029,7 @@ class ObjectHydratorTest extends HydrationTestCase
'Proxies',
ProxyFactory::AUTOGENERATE_ALWAYS,
) extends ProxyFactory {
public function getProxy(string $className, array $identifier): InternalProxy
public function getProxy(string $className, array $identifier): object
{
TestCase::assertSame(ECommerceShipping::class, $className);
TestCase::assertSame(['id' => 42], $identifier);
@@ -1084,7 +1083,7 @@ class ObjectHydratorTest extends HydrationTestCase
'Proxies',
ProxyFactory::AUTOGENERATE_ALWAYS,
) extends ProxyFactory {
public function getProxy(string $className, array $identifier): InternalProxy
public function getProxy(string $className, array $identifier): object
{
TestCase::assertSame(ECommerceShipping::class, $className);
TestCase::assertSame(['id' => 42], $identifier);

View File

@@ -119,9 +119,9 @@ class BasicInheritanceMappingTest extends OrmTestCase
$class2 = unserialize(serialize($class));
$class2->wakeupReflection(new RuntimeReflectionService());
self::assertArrayHasKey('mapped1', $class2->reflFields);
self::assertArrayHasKey('mapped2', $class2->reflFields);
self::assertArrayHasKey('mappedRelated1', $class2->reflFields);
self::assertArrayHasKey('mapped1', $class2->propertyAccessors);
self::assertArrayHasKey('mapped2', $class2->propertyAccessors);
self::assertArrayHasKey('mappedRelated1', $class2->propertyAccessors);
}
#[Group('DDC-1203')]

View File

@@ -10,10 +10,10 @@ use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor;
use Doctrine\ORM\Mapping\Table;
use Doctrine\Tests\OrmTestCase;
use PHPUnit\Framework\Attributes\Group;
use ReflectionProperty;
class ClassMetadataLoadEventTest extends OrmTestCase
{
@@ -26,8 +26,8 @@ class ClassMetadataLoadEventTest extends OrmTestCase
$evm->addEventListener(Events::loadClassMetadata, $this);
$classMetadata = $metadataFactory->getMetadataFor(LoadEventTestEntity::class);
self::assertTrue($classMetadata->hasField('about'));
self::assertArrayHasKey('about', $classMetadata->reflFields);
self::assertInstanceOf(ReflectionProperty::class, $classMetadata->reflFields['about']);
self::assertArrayHasKey('about', $classMetadata->propertyAccessors);
self::assertInstanceOf(PropertyAccessor::class, $classMetadata->propertyAccessors['about']);
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void

View File

@@ -56,6 +56,7 @@ use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use ReflectionClass;
use stdClass;
use function array_keys;
use function assert;
use function class_exists;
use function count;
@@ -80,7 +81,7 @@ class ClassMetadataTest extends OrmTestCase
$cm->initializeReflection(new RuntimeReflectionService());
// Test initial state
self::assertTrue(count($cm->getReflectionProperties()) === 0);
self::assertTrue(count($cm->getPropertyAccessors()) === 0);
self::assertInstanceOf(ReflectionClass::class, $cm->reflClass);
self::assertEquals(CmsUser::class, $cm->name);
self::assertEquals(CmsUser::class, $cm->rootEntityName);
@@ -106,7 +107,7 @@ class ClassMetadataTest extends OrmTestCase
$cm->wakeupReflection(new RuntimeReflectionService());
// Check state
self::assertTrue(count($cm->getReflectionProperties()) > 0);
self::assertTrue(count($cm->getPropertyAccessors()) > 0);
self::assertEquals('Doctrine\Tests\Models\CMS', $cm->namespace);
self::assertInstanceOf(ReflectionClass::class, $cm->reflClass);
self::assertEquals(CmsUser::class, $cm->name);
@@ -989,7 +990,7 @@ class ClassMetadataTest extends OrmTestCase
self::assertInstanceOf(MyArrayObjectEntity::class, $classMetadata->newInstance());
}
public function testWakeupReflectionWithEmbeddableAndStaticReflectionService(): void
public function testWakeupReflectionWithEmbeddable(): void
{
if (! class_exists(StaticReflectionService::class)) {
self::markTestSkipped('This test is not supported by the current installed doctrine/persistence version');
@@ -999,7 +1000,7 @@ class ClassMetadataTest extends OrmTestCase
$classMetadata->mapEmbedded(
[
'fieldName' => 'test',
'fieldName' => 'embedded',
'class' => TestEntity1::class,
'columnPrefix' => false,
],
@@ -1009,14 +1010,14 @@ class ClassMetadataTest extends OrmTestCase
'fieldName' => 'test.embeddedProperty',
'type' => 'string',
'originalClass' => TestEntity1::class,
'declaredField' => 'test',
'originalField' => 'embeddedProperty',
'declaredField' => 'embedded',
'originalField' => 'name',
];
$classMetadata->mapField($field);
$classMetadata->wakeupReflection(new StaticReflectionService());
self::assertEquals(['test' => null, 'test.embeddedProperty' => null], $classMetadata->getReflectionProperties());
self::assertEquals(['embedded', 'test.embeddedProperty'], array_keys($classMetadata->getPropertyAccessors()));
}
public function testGetColumnNamesWithGivenFieldNames(): void

Some files were not shown because too many files have changed in this diff Show More