Compare commits

..

218 Commits
3.3.4 ... 3.5.2

Author SHA1 Message Date
Grégoire Paris
5a541b8b3a Merge pull request #12121 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-08-08 19:00:40 +02:00
Grégoire Paris
9fb9cc46e4 Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-08-08 09:03:30 +02:00
Grégoire Paris
c322c71cd4 Merge pull request #12120 from greg0ire/fix-broken-comments
Fix broken comments
2025-08-08 08:55:44 +02:00
Grégoire Paris
3c733a2fee Add missing phpdoc 2025-08-08 08:34:07 +02:00
Grégoire Paris
5984ad586a Fix broken phpdoc comments 2025-08-08 08:33:39 +02:00
Grégoire Paris
ee2c3a506b Merge pull request #12097 from mvorisek/add_fixed_id_insert_count_query_test
Add 2nd level cache test for insert without post-inserted ID
2025-08-08 08:26:23 +02:00
Grégoire Paris
781ed30926 Merge pull request #12108 from greg0ire/address-deprecations
Address deprecations
2025-08-08 08:25:29 +02:00
Grégoire Paris
04694a9f7b Merge pull request #11835 from gseidel/fix-pre-persist-call-persist
fix: calling scheduleForInsert twice
2025-08-07 06:25:35 +02:00
Grégoire Paris
257c5094c4 Use not nullable columns when part of the primary key
Using a nullable column that references another table as part of a
primary key makes no sense, and is ignored by DBAL. Let us ignore it at
the ORM level.
2025-08-06 23:06:28 +02:00
Alexander M. Turek
66e0e92816 Don't partially mock the AbstractPlatform class (#12114) 2025-08-06 18:12:23 +02:00
Michael Voříšek
5b2060e25f Add 2nd level cache test for insert without post-inserted ID
Inserts without post-inserted ID can be sent to DB grouped together
hence the extra test.
2025-08-06 08:22:48 +02:00
Grégoire Paris
64444dcfd5 Merge pull request #12088 from greg0ire/quote-parts
Quote parts of the table name
2025-08-05 08:05:51 +02:00
Alexander M. Turek
eb2cd5375c Move LazyGhost deprecation to ProxyFactory (#12101) 2025-08-04 23:48:11 +02:00
Grégoire Paris
de7140e105 Address deprecations from doctrine/dbal (#12098)
- Non-standard flags are deprecated.
- Index::getColumns() is deprecated.
2025-08-04 23:47:55 +02:00
Grégoire Paris
39e35fc06c Merge pull request #12099 from alexislefebvre/2.20.x-update-supported-branches-on-README
doc: update supported branches on README (2.20.x)
2025-08-04 16:59:49 +02:00
Alexis Lefebvre
7f061c3870 doc: update supported branches on README 2025-08-04 16:38:55 +02:00
Grégoire Paris
74495711fb Merge pull request #11934 from mvorisek/fix_joined_subclass_persister_insert_of_multiple_entities
Fix JoinedSubclassPersister when multiple entities are inserted
2025-08-02 08:34:29 +02:00
Michael Voříšek
97a7cb8d2f Unify JoinedSubclassPersister dequeue
Fix JoinedSubclassPersister as BasicEntityPersister was already fixed in GH-10735.

The fix can be verified by modifying UnitOfWork to execute `BasicEntityPersister::executeInserts()` for multiple entities at once for the same entity class/persister instance - https://github.com/doctrine/orm/blob/2.20.3/src/UnitOfWork.php#L1186 - then reproducible on `Doctrine\Tests\ORM\Functional\Ticket\GH10531Test::testInserts` test.

As extending/modifying UnitOfWork in tests in not easily possible, I submit this fix for v2.x without a test.
2025-08-01 15:31:18 +02:00
Grégoire Paris
85d66de9df Improve comment
This comment is rendered useless by the phpdoc comment below it.
Instead, we can comment on what exactly "quoted" means.
2025-07-31 22:54:44 +02:00
Grégoire Paris
1b98be31ce Convert test into 2 unit tests
That test was testing too many thing and not really making it clear what
the expected output was, given some output. Instead, let us create 2
tests, each pertaining to the class under test.
2025-07-31 22:53:35 +02:00
Grégoire Paris
61f2752a80 Quote parts of the table name
In aa141bf001, I wrongly assumed that
$tableName would never contain a dot as I was not able to write a test
that caused that to happen.

The secret recipe appears to be to define a schema and to quote the
table name.

To fix it for the table name, I am calling quoteSingleIdentifier()
before doing the concatenation between schema name and table name.

To fix it for the sequence name, which seems only useful when using DBAL
3 for some reason, I reuse some of the logic of the deprecated method.

Fixes #12041
2025-07-31 21:49:43 +02:00
Grégoire Paris
f3371e1773 Merge pull request #12094 from greg0ire/simplify-test
Remove if statement
2025-07-31 17:12:39 +02:00
Grégoire Paris
6476894dc4 Remove if statement
Tests should not have conditional logic, and since a41c6d3, the else
branch of this conditional statement is dead.
2025-07-31 09:01:52 +02:00
Grégoire Paris
e0052390e1 Merge pull request #12087 from mvorisek/improve_basic_entity_persister
Improve BasicEntityPersister to be more flexible and cleaner
2025-07-30 09:44:24 +02:00
Michael Voříšek
8c6419e0e0 Prefer strict empty-array comparison over empty() call 2025-07-29 15:15:31 +02:00
Michael Voříšek
6f5ce1aca2 BasicEntityPersister: refactor $values variable into $placeholders
The new variable name is much more clearer.
2025-07-29 15:15:31 +02:00
Michael Voříšek
98e7a53b42 Remove BasicEntityPersister::$insertSql cache property
When the persister is extended to do a multi update, the caching is not
wanted. The impact is minimal as the CPU/time overhead per query is
much bigger and the prepared statement is not cached anyway.
2025-07-29 15:15:31 +02:00
Gerhard Seidel
3aaaf37dfb fix: PrePersistEventTest typos and unnecessary comments 2025-07-29 14:40:20 +02:00
Grégoire Paris
154a4652ee Merge pull request #12086 from mvorisek/add_cache_rw_strict_locking_test
Add functional strict-locking 2nd level cache test
2025-07-29 11:48:25 +02:00
Michael Voříšek
ae7489ff19 Add functional strict-locking 2nd level cache test 2025-07-28 12:14:50 +02:00
Grégoire Paris
0f32569a7a Merge pull request #12083 from greg0ire/depr-reflFields
Deprecate ClassMetadata::$reflFields
2025-07-27 23:19:29 +02:00
Gregoire PARIS
d99f74c704 Deprecate ClassMetadata::$reflFields
It is replaced with property accessors since
https://github.com/doctrine/orm/pull/11659
2025-07-24 09:55:19 +02:00
Grégoire Paris
62ca8424d8 Merge pull request #12080 from greg0ire/3.5.x
Merge 2.20.x up into 3.5.x
2025-07-22 09:35:03 +02:00
Gregoire PARIS
3f2209a571 Merge remote-tracking branch 'origin/2.20.x' into 3.5.x 2025-07-22 09:09:21 +02:00
Grégoire Paris
1ee01f4473 Merge pull request #12078 from stlgaits/2.20.x
Fix embedded classes display in orm:mapping:command output
2025-07-22 08:48:52 +02:00
stlgaits
8a9ed138a8 Fix embedded classes display in orm:mapping:command output 2025-07-21 15:44:46 +02:00
Grégoire Paris
e714b1a2fc Merge pull request #12075 from andrew-demb/patch-1
📖 Actualize code block to be compatible with DBAL v4, use modern PHP
2025-07-15 18:16:50 +02:00
Benjamin Morel
ec0bf05853 Use PHP attributes syntax in schema validator message (#12074) 2025-07-15 00:24:16 +02:00
Andrii Dembitskyi
dc58aa3ea1 📖 Actualize code block to be compatible with DBAL v4, use modern PHP 2025-07-14 20:15:18 +03:00
Grégoire Paris
23b74e4f8b Merge pull request #12063 from wmouwen/test/gh-10788
Proxy class with BackedEnum as primary key does not convert the enum
2025-07-14 10:04:21 +02:00
Willem Mouwen
d2b699e6f5 fix: Convert BackedEnum to scalar value when binding a parameter 2025-07-12 20:50:25 +02:00
Willem Mouwen
0338d69324 test: Store an entity with a proxy association that has a BackedEnum primary identifier 2025-07-12 20:50:19 +02:00
Grégoire Paris
2c01dac173 Merge pull request #12072 from greg0ire/update-baseline
Update baseline because of doctrine/dbal 4.3.0
2025-07-12 09:46:50 +02:00
Grégoire Paris
137ecb491a Update baseline because of doctrine/dbal 4.3.0
This should fix the build. Maybe some of the reported issues can be
addressed, but if that is the case, it should probably be done on the
next minor branch.
2025-07-11 18:42:54 +02:00
Grégoire Paris
f38ee09082 Merge pull request #12062 from janedbal/binary-id-eager-fetch-reupload
Fix unhandled ParameterType case for binary PKs
2025-07-10 17:05:57 +02:00
Jan Nedbal
6ab858a5c5 Apply suggestion from @greg0ire
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2025-07-10 16:54:02 +02:00
Jan Nedbal
3dca27ce0d Fix unhandled ParameterType case for binary PKs
Add proper handling for binary primary key parameter types that were
  previously causing runtime exceptions. The existing parameter type
  switch statement was missing a case for binary types, leading to
  unhandled scenarios when working with binary primary keys.

  This ensures consistent parameter type handling across all supported
  primary key data types in the ORM.
2025-07-07 12:07:52 +02:00
Grégoire Paris
e19704e1f8 Merge pull request #12056 from greg0ire/switch-to-tags
Remove branchName from unmaintained branch
2025-07-04 07:50:39 +02:00
Alexis Lefebvre
41ea59ac66 chore: use a shorter name for CI on GitHub Actions (#12055) 2025-07-04 00:19:01 +02:00
Alexis Lefebvre
e605e6d569 doc: add links to GitHub Actions on README (#12054) 2025-07-04 00:13:51 +02:00
Grégoire Paris
9437675d3b Remove branchName from unmaintained branch
Since https://github.com/doctrine/doctrine-website/pull/372, they are no
longer necessary, it's possible to rely on tags. Once this is merged,
the branches can be removed.
2025-07-02 23:32:01 +02:00
Grégoire Paris
63409d638c Merge pull request #12048 from greg0ire/update-branch-metadata
Update branch metadata for 3.5.0
2025-07-02 08:12:04 +02:00
Grégoire Paris
9b9160b206 Update branch metadata for 3.5.0
3.5.0 has been released.

- 3.6.x is the new upcoming branch;
- 3.5.x is now the current branch;
- 3.4.x is no longer maintained.
2025-07-01 19:45:42 +02:00
Grégoire Paris
6deec3655b Merge pull request #12046 from greg0ire/3.5.x
Merge 3.4.x up into 3.5.x
2025-07-01 19:40:53 +02:00
Grégoire Paris
7f40422d21 Merge remote-tracking branch 'origin/3.4.x' into 3.5.x 2025-07-01 19:13:12 +02:00
Grégoire Paris
e67fa5388b Merge pull request #12043 from beberlei/Bugfix-DisableNativeLazyLogicException
Only throw PHP 8.4 requirement exception when enabling native lazy objects.
2025-06-30 23:43:15 +02:00
Grégoire Paris
80053336c9 Merge pull request #12044 from doctrine/3.4.x
Merge branch 3.4.x into 3.5.x
2025-06-30 21:53:04 +02:00
Grégoire Paris
dddcc507ef Merge pull request #12039 from xabbuh/pr-12036
do not register the legacy proxy class name resolver with enabled native lazy ghost
2025-06-30 20:38:34 +02:00
Christian Flothmann
b41d9da88d do not register the legacy proxy class name resolver with enabled native lazy ghost 2025-06-30 19:14:11 +02:00
Benjamin Eberlei
c04bfb78b7 Only throw PHP 8.4 requirement exception when enabling native lazy objects. 2025-06-30 19:01:49 +02:00
Grégoire Paris
8a5dfc86d4 Merge pull request #12037 from stlgaits/mapping-describe-completion
Add console completion for entityName param of orm:mapping:describe c…
2025-06-29 18:26:55 +02:00
Grégoire Paris
79e103c07e Merge pull request #11978 from Ocramius/feature/#11977-batch-handling-of-inserts-with-given-ids
#11977 implemented batching of `INSERT` operations in `UnitOfWork#executeInserts()` so that `EntityPersister#executeInserts()` calls are reduced
2025-06-28 22:19:54 +02:00
stlgaits
5afadf163a Add console completion for entityName param of orm:mapping:describe command 2025-06-28 11:27:52 +02:00
Grégoire Paris
edfaa37228 Merge pull request #12036 from greg0ire/depr-proxy-autoload
Deprecate proxy autoloader and class name resolver
2025-06-28 11:09:37 +02:00
Grégoire Paris
ea056e98ba Deprecate proxy autoloader and class name resolver
These are only needed when not using native lazy objects.
2025-06-27 19:24:23 +02:00
Grégoire Paris
bab5771e98 Merge pull request #12034 from doctrine/3.4.x
Merge 3.4.x up into 3.5.x
2025-06-27 18:32:26 +02:00
Grégoire Paris
ee919d6231 Merge pull request #12030 from greg0ire/test-w-lazy-o
Rework tests and benchmarks to work with lazy objects
2025-06-27 18:13:24 +02:00
Grégoire Paris
04c390693a Merge pull request #12033 from greg0ire/remove-assert
Remove wrong assertion
2025-06-27 15:55:40 +02:00
Grégoire Paris
49293c4d48 Merge pull request #12032 from doctrine/3.4.x-merge-up-into-3.5.x_dG7qI4BR
Merge release 3.4.3 into 3.5.x
2025-06-27 14:44:44 +02:00
Gregoire PARIS
8d9e2e7d4e Remove wrong assertion
When using native lazy objects, it is plain wrong.
2025-06-27 14:42:37 +02:00
Grégoire Paris
ef607f26c2 Merge pull request #12031 from doctrine/stof-patch-1
Clean the handling of proxy initialization in the UnitOfWork
2025-06-27 14:14:15 +02:00
Gregoire PARIS
ed543a205c Rework tests and benchmarks to work with lazy objects
These tests and benchmarks are still relevant with lazy objects.
I am not setting up an extra job to test phpbench without native lazy
objects. Instead, I'm bumping the PHP version to 8.4 so that native lazy
objects are in use.
2025-06-27 14:12:58 +02:00
Christophe Coevoet
de1c28bb16 Clean the handling of proxy initialization in the UnitOfWork
Using the VarExporter Hydrator to assign default values of properties when marking an entity as initialized is needed only when using var-exporter proxies.
For lazy objects, this behavior is already provided by `ReflectionClass::markLazyObjectAsInitialized`
2025-06-27 13:58:03 +02:00
Grégoire Paris
60ff966d54 Merge pull request #12022 from greg0ire/depr-proxy-dir
Provide upgrade path to new ORMSetup::create* signature
2025-06-27 08:12:42 +02:00
Grégoire Paris
33684253c3 Merge pull request #12026 from doctrine/3.4.x-merge-up-into-3.5.x_yt3lc4tn
Merge release 3.4.2 into 3.5.x
2025-06-26 21:07:39 +02:00
Grégoire Paris
b4ca0cd5fb Merge pull request #12024 from greg0ire/3.4.x
Merge 2.20.x up into 3.4.x
2025-06-26 20:51:01 +02:00
Grégoire Paris
a49c1beb93 Merge remote-tracking branch 'origin/2.20.x' into 3.4.x 2025-06-26 20:38:31 +02:00
Grégoire Paris
76852cfef3 Provide upgrade path to new ORMSetup::create* signature
Currently we have ORMSetup::create*Configuration methods with a
$proxyDir argument that is used to configure the proxy directory, but
also as a seed for generating a namespace for cache systems.

Since these methods could be used with named arguments, renaming the
argument is not really an option and we need separate methods.
2025-06-26 00:14:59 +02:00
Grégoire Paris
3bd89caf36 use lowercase for word in upgrade guide 2025-06-25 23:27:18 +02:00
Grégoire Paris
eb2e7d959c Merge pull request #12020 from greg0ire/depr-legacy-proxy
Deprecate more proxies-related methods or calls
2025-06-25 23:17:56 +02:00
Grégoire Paris
a4b20356f4 Merge pull request #11988 from jannes-io/3.4.x
Add index mapping to column
2025-06-25 11:17:22 +02:00
Grégoire Paris
2550b2d1de Deprecate more proxies-related methods or calls 2025-06-25 00:04:27 +02:00
jannes
e94e1ab126 Add index mapping to Column
Adds a new option to Column mapping to add indexes to class fields
directly instead of having to use the Index() class attribute.
This allows users to define indexes in traits
where access to the class isn't available.
Fixes #11982
2025-06-24 19:50:47 +02:00
Grégoire Paris
6307b4fa7d Merge pull request #8012 from sgehrig/bug/#8011-ordering-with-arithmetic-expression
Bug/#8011 ordering with arithmetic expression
2025-06-24 19:50:46 +02:00
Grégoire Paris
19e1a64a91 Merge pull request #12014 from greg0ire/3.5.x
Merge 3.4.1 up into 3.5.x
2025-06-21 13:57:03 +02:00
Grégoire Paris
082e776e91 Merge remote-tracking branch 'origin/3.4.x' into 3.5.x 2025-06-21 13:14:14 +02:00
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
bbde41f712 Merge pull request #12005 from greg0ire/depr-no-lazy-objects
Deprecate not using native lazy objects on PHP 8.4+
2025-06-18 07:24:56 +02:00
Alexander M. Turek
8c0994f35f Detect DBAL's number type (#11781) 2025-06-18 02:43:47 +02:00
Grégoire Paris
3d390bc053 Deprecate not using native lazy objects on PHP 8.4+ 2025-06-18 00:14:15 +02:00
Grégoire Paris
16f1be7f10 Merge pull request #12004 from doctrine/3.4.x
Merge 3.4.x up into 3.5.x
2025-06-18 00:12:23 +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
1334162a56 Merge pull request #11989 from greg0ire/late-depr
Deprecate methods for configuring no longer configurable features
2025-06-16 08:37:59 +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
68ec3ebaa3 Remove trailing whitespace 2025-06-14 18:14:58 +02:00
Grégoire Paris
4f4ed2f242 Deprecate methods for configuring no longer configurable features
In 3.0.0, it is no longer possible to disable lazy ghost objects, and
likewise, it is no longer possible to disable rejecting id collisions in
the identity map, so let us deprecate the related methods.
I was supposed to do this in 3.1.0.
2025-06-14 18:14:54 +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
Marco Pivetta
79cc70a62f #11977 expanded test coverage to check interleaved assigned-id vs generated-id entities
As noted by @bendavies

Ref: https://github.com/doctrine/orm/pull/11978#discussion_r2141143273
2025-06-11 23:44:24 +02:00
Marco Pivetta
4e6b5a1b0b #11977 provided method documentation / example, as per @greg0ire's feedback
Ref: https://github.com/doctrine/orm/pull/11978#discussion_r2140881217
2025-06-11 22:53:22 +02:00
Marco Pivetta
21b144fff9 #11977 removed unused type-hint, which can be completely inferred by the parameters 2025-06-11 22:50:15 +02:00
Marco Pivetta
658940de38 #11977 only perform batching if/when the AssignedGenerator is in use
The `SequenceGenerator` is potentially used for PostgreSQL table auto-generated fields, but
the `SequenceGenerator` is not a **POST**-insert generator.

Because the `SequenceGenerator` is used in the middle of `INSERT` operations performed
by persisters, we cannot rely on it in batching operations: disabling it, so we get a green
test suite on PostgreSQL.

This change makes `GH10531Test` pass on PostgreSQL: see #10531
2025-06-11 18:26:20 +02:00
Marco Pivetta
ad487370f5 #11977 hardened InsertBatchTest to check entity types of sequential batches 2025-06-11 18:16:45 +02:00
Marco Pivetta
259f83b549 #11977 added test coverage verifying that persisters are being used to batch INSERTs 2025-06-11 17:36:24 +02:00
Marco Pivetta
4a24860dcf #11977 isolated INSERT batch generation to own @internal performance-sensitive class 2025-06-11 17:18:23 +02:00
Marco Pivetta
116cdf8661 #11977 implemented simplistic (and ugly) batch handing of INSERT operations in UnitOfWork#executeInserts()
This logic also brings a minor benefit in reducing the number of times `ListenersInvoker#getSubscribedSystems`
is queried.

TODOs:

* [ ] integration test this - it is expected to reduce the number of `EntityPersister#executeInserts()` calls
* [ ] refactor this by creating a new `@internal` class for the batch, and perhaps batch via a generator
* [ ] reduce amount of repeated `getClassMetadata()` calls
* [ ] reduce overall size of `UnitOfWork` code, instead of increasing it
2025-06-11 15:43:04 +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
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
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
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
528b8837e1 Merge pull request #11929 from doctrine/2.20.x-merge-up-into-2.21.x_KkdqS0u7
Merge release 2.20.3 into 2.21.x
2025-05-02 21:57:23 +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
Stefan Gehrig
067ad51b3f fixes sqlite sql inconsistency 2025-03-17 08:48:30 +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
Stefan Gehrig
00c77213fb fixes codesniffer violation 2025-03-15 09:42:21 +01:00
Stefan Gehrig
c68b8f90b3 adds a test for postgres that uses a HIDDEN result variable for ordering based on arithmetic expression 2025-03-05 09:28:52 +01:00
Stefan Gehrig
aa4f9ce9e9 CS fix based on PHP_CodeSniffer report 2025-03-05 09:22:57 +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
Stefan Gehrig
d96fc23327 skips tests when running on postgres 2025-02-27 10:30:21 +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
Gerhard Seidel
4fb044d5f6 fix: cs 2025-02-20 10:01:35 +08: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
Gerhard Seidel
2a953c5e2b fix: PrePersistEventTest and cs 2025-02-17 14:01:08 +08: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
Gerhard Seidel
abc6a40ccb fix: calling scheduleForInsert twice
If scheduleForInsert was called in prePersist hook already, then persistNew need to check this case first, otherwise a ORMInvalidArgumentException will be thrown
2025-02-14 12:45:13 +08: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
73e68f3c7d Merge pull request #11821 from doctrine/2.20.x-merge-up-into-2.21.x_8O8nHxqC
Merge release 2.20.2 into 2.21.x
2025-02-04 20:24:01 +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
Alexander M. Turek
73777d0bd4 Merge branch '2.20.x' into 2.21.x
* 2.20.x:
  Introduce testNotListedValueInEnumArray
  Fix documentation for JoinColumn nullable (#11798)
  Ignore deprecations from doctrine/common
  Fix fields of transient classes being considered duplicate with `reportFieldsWhereDeclared`
2025-01-26 19:56:20 +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
Stefan Gehrig
ec6d1b9f72 fixes whitespace
Signed-off-by: Stefan Gehrig <stefan.gehrig.hn@googlemail.com>
2025-01-07 08:51:19 +01:00
Stefan Gehrig
d809fed52a fixes code sniffer complaints
Signed-off-by: Stefan Gehrig <stefan.gehrig.hn@googlemail.com>
2025-01-07 08:48:42 +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
Stefan Gehrig
0e4786dfa8 adds testcases for order by items enclosed in ((...)) (double brackets - just one bracket does not work)
just one bracket (...) gives

Exception : [Doctrine\ORM\Query\QueryException] [Syntax Error] line 0, col xx: Error: Expected Doctrine\ORM\Query\Lexer::T_IDENTIFIER, got '('
2025-01-03 10:45:08 +01:00
Stefan Gehrig
c429262f02 adds detection of literals/result variables at the beginning of an order by item with arithmetic expression
Not sure whether this covers the whole problem regarding complex expressions in order by items but it fixes the provided test cases
2025-01-03 10:45:07 +01:00
Stefan Gehrig
f4fdcbcdcb adds more test cases 2025-01-03 10:44:17 +01:00
Stefan Gehrig
b0806469d5 adds test case for GH issue #8011 2025-01-03 10:44:17 +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
Grégoire Paris
e89b58a13f Merge pull request #11771 from doctrine/2.20.x-merge-up-into-2.21.x_3Yg2ZYgM
Merge release 2.20.1 into 2.21.x
2024-12-19 08:16:04 +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
2b94ec18b9 Merge pull request #11759 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-12-08 14:33:31 +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
2a662149f4 Merge pull request #11754 from doctrine/2.20.x
Merge 2.20.x up into 2.21.x
2024-12-07 15:39:29 +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
141 changed files with 4938 additions and 713 deletions

View File

@@ -12,32 +12,39 @@
"upcoming": true
},
{
"name": "3.4",
"branchName": "3.4.x",
"slug": "3.4",
"name": "3.6",
"branchName": "3.6.x",
"slug": "3.6",
"upcoming": true
},
{
"name": "3.3",
"branchName": "3.3.x",
"slug": "3.3",
"name": "3.5",
"branchName": "3.5.x",
"slug": "3.5",
"current": true
},
{
"name": "3.4",
"slug": "3.4",
"maintained": false
},
{
"name": "3.3",
"slug": "3.3",
"maintained": false
},
{
"name": "3.2",
"branchName": "3.2.x",
"slug": "3.2",
"maintained": false
},
{
"name": "3.1",
"branchName": "3.1.x",
"slug": "3.1",
"maintained": false
},
{
"name": "3.0",
"branchName": "3.0.x",
"slug": "3.0",
"maintained": false
},
@@ -55,61 +62,51 @@
},
{
"name": "2.19",
"branchName": "2.19.x",
"slug": "2.19",
"maintained": false
},
{
"name": "2.18",
"branchName": "2.18.x",
"slug": "2.18",
"maintained": false
},
{
"name": "2.17",
"branchName": "2.17.x",
"slug": "2.17",
"maintained": false
},
{
"name": "2.16",
"branchName": "2.16.x",
"slug": "2.16",
"maintained": false
},
{
"name": "2.15",
"branchName": "2.15.x",
"slug": "2.15",
"maintained": false
},
{
"name": "2.14",
"branchName": "2.14.x",
"slug": "2.14",
"maintained": false
},
{
"name": "2.13",
"branchName": "2.13.x",
"slug": "2.13",
"maintained": false
},
{
"name": "2.12",
"branchName": "2.12.x",
"slug": "2.12",
"maintained": false
},
{
"name": "2.11",
"branchName": "2.11.x",
"slug": "2.11",
"maintained": false
},
{
"name": "2.10",
"branchName": "2.10.x",
"slug": "2.10",
"maintained": false
}

View File

@@ -1,4 +1,4 @@
name: "Continuous Integration"
name: "CI"
on:
pull_request:
@@ -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

@@ -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.6.x][3.6] | [3.5.x][3.5] | [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 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][3.5 image]][3.5 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][3.5 coverage image]][3.5 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
@@ -18,21 +18,26 @@ without requiring unnecessary code duplication.
[4.0 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0.x
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
[4.0 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A4.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.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
[3.6 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.6.x
[3.6]: https://github.com/doctrine/orm/tree/3.6.x
[3.6 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.6.x
[3.6 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.6.x/graph/badge.svg
[3.6 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.6.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 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.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
[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 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.21.x
[2.21 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.21.x/graph/badge.svg
[2.21 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.21.x
[2.20 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.20.x
[2.20]: https://github.com/doctrine/orm/tree/2.20.x
[2.20 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.20.x
[2.20 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.20.x/graph/badge.svg
[2.20 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.20.x

View File

@@ -1,8 +1,76 @@
# Upgrade to 3.5
## Deprecate not using native lazy objects on PHP 8.4+
Having native lazy objects disabled on PHP 8.4+ is deprecated and will not be
possible in 4.0.
You can enable them through configuration:
```php
$config->enableNativeLazyObjects(true);
```
As a consequence, methods, parameters and commands related to userland lazy
objects have been deprecated on PHP 8.4+:
- `Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand`
- `Doctrine\ORM\Configuration::getAutoGenerateProxyClasses()`
- `Doctrine\ORM\Configuration::getProxyDir()`
- `Doctrine\ORM\Configuration::getProxyNamespace()`
- `Doctrine\ORM\Configuration::setAutoGenerateProxyClasses()`
- `Doctrine\ORM\Configuration::setProxyDir()`
- `Doctrine\ORM\Configuration::setProxyNamespace()`
- Passing more than one argument to `Doctrine\ORM\Proxy\ProxyFactory::__construct()`
Additionally, some methods of ORMSetup have been deprecated in favor of a new
counterpart.
- `Doctrine\ORM\ORMSetup::createAttributeMetadataConfiguration()` is deprecated in favor of
`Doctrine\ORM\ORMSetup::createAttributeMetadataConfig()`
- `Doctrine\ORM\ORMSetup::createXMLMetadataConfiguration()` is deprecated in favor of
`Doctrine\ORM\ORMSetup::createXMLMetadataConfig()`
- `Doctrine\ORM\ORMSetup::createConfiguration()` is deprecated in favor of
`Doctrine\ORM\ORMSetup::createConfig()`
## Deprecate methods for configuring no longer configurable features
Since 3.0, lazy ghosts are enabled unconditionally, and so is rejecting ID
collisions in the identity map.
As a consequence, the following methods are deprecated and will be removed in 4.0:
* `Doctrine\ORM\Configuration::setLazyGhostObjectEnabled()`
* `Doctrine\ORM\Configuration::isLazyGhostObjectEnabled()`
* `Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()`
* `Doctrine\ORM\Configuration::isRejectIdCollisionInIdentityMapEnabled()`
# 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.
Companion accessor methods are deprecated as well.
# Upgrade to 3.3
## Deprecate `DatabaseDriver`
@@ -13,7 +81,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 +192,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.

View File

@@ -46,17 +46,18 @@ entities:
#[Entity]
class Article
{
const STATUS_VISIBLE = 'visible';
const STATUS_INVISIBLE = 'invisible';
public const STATUS_VISIBLE = 'visible';
public const STATUS_INVISIBLE = 'invisible';
#[Column(type: "string")]
private $status;
public function setStatus($status)
public function setStatus(string $status): void
{
if (!in_array($status, array(self::STATUS_VISIBLE, self::STATUS_INVISIBLE))) {
if (!in_array($status, [self::STATUS_VISIBLE, self::STATUS_INVISIBLE], true)) {
throw new \InvalidArgumentException("Invalid status");
}
$this->status = $status;
}
}
@@ -92,37 +93,33 @@ For example for the previous enum type:
class EnumVisibilityType extends Type
{
const ENUM_VISIBILITY = 'enumvisibility';
const STATUS_VISIBLE = 'visible';
const STATUS_INVISIBLE = 'invisible';
private const ENUM_VISIBILITY = 'enumvisibility';
private const STATUS_VISIBLE = 'visible';
private const STATUS_INVISIBLE = 'invisible';
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
{
return "ENUM('visible', 'invisible')";
}
public function convertToPHPValue($value, AbstractPlatform $platform)
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
{
return $value;
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): string
{
if (!in_array($value, array(self::STATUS_VISIBLE, self::STATUS_INVISIBLE))) {
if (!in_array($value, [self::STATUS_VISIBLE, self::STATUS_INVISIBLE], true)) {
throw new \InvalidArgumentException("Invalid status");
}
return $value;
}
public function getName()
public function getName(): string
{
return self::ENUM_VISIBILITY;
}
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}
You can register this type with ``Type::addType('enumvisibility', 'MyProject\DBAL\EnumVisibilityType');``.
@@ -151,37 +148,33 @@ You can generalize this approach easily to create a base class for enums:
abstract class EnumType extends Type
{
protected $name;
protected $values = array();
protected $values = [];
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
{
$values = array_map(function($val) { return "'".$val."'"; }, $this->values);
$values = array_map(fn($val) => "'".$val."'", $this->values);
return "ENUM(".implode(", ", $values).")";
}
public function convertToPHPValue($value, AbstractPlatform $platform)
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
{
return $value;
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed
{
if (!in_array($value, $this->values)) {
if (!in_array($value, $this->values, true)) {
throw new \InvalidArgumentException("Invalid '".$this->name."' value.");
}
return $value;
}
public function getName()
public function getName(): string
{
return $this->name;
}
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}
With this base class you can define an enum as easily as:
@@ -194,5 +187,5 @@ With this base class you can define an enum as easily as:
class EnumVisibilityType extends EnumType
{
protected $name = 'enumvisibility';
protected $values = array('visible', 'invisible');
protected $values = ['visible', 'invisible'];
}

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

@@ -175,6 +175,10 @@ Optional parameters:
- **unique**: Boolean value to determine if the value of the column
should be unique across all rows of the underlying entities table.
- **index**: Boolean value to generate an index for this column.
For more advanced usages, take a look at :ref:`#[Index] <attrref_index>`.
If not specified, default value is ``false``.
- **nullable**: Determines if NULL values allowed for this column.
If not specified, default value is ``false``.
@@ -245,6 +249,9 @@ Examples:
#[Column(type: "string", length: 32, unique: true, nullable: false)]
protected $username;
#[Column(type: "string", index: true)]
protected $firstName;
#[Column(type: "string", columnDefinition: "CHAR(2) NOT NULL")]
protected $country;
@@ -676,6 +683,7 @@ 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)

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

@@ -56,7 +56,8 @@ access point to ORM functionality provided by Doctrine.
'dbname' => 'foo',
];
$config = ORMSetup::createAttributeMetadataConfiguration($paths, $isDevMode);
$config = ORMSetup::createAttributeMetadataConfig($paths, $isDevMode);
// on PHP < 8.4, use ORMSetup::createAttributeMetadataConfiguration() instead
$connection = DriverManager::getConnection($dbParams, $config);
$entityManager = new EntityManager($connection, $config);
@@ -66,7 +67,8 @@ Or if you prefer XML:
<?php
$paths = ['/path/to/xml-mappings'];
$config = ORMSetup::createXMLMetadataConfiguration($paths, $isDevMode);
$config = ORMSetup::createXMLMetadataConfig($paths, $isDevMode);
// on PHP < 8.4, use ORMSetup::createXMLMetadataConfiguration() instead
$connection = DriverManager::getConnection($dbParams, $config);
$entityManager = new EntityManager($connection, $config);

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

@@ -209,6 +209,7 @@ Field & Association Getters
- ``isUniqueField($fieldName)``
- ``isNullable($fieldName)``
- ``isIndexed($fieldName)``
- ``getColumnName($fieldName)``
- ``getFieldMapping($fieldName)``
- ``getAssociationMapping($fieldName)``

View File

@@ -84,7 +84,7 @@ The following Commands are currently available:
- ``orm:clear-cache:result`` Clear result cache of the various
cache drivers.
- ``orm:generate-proxies`` Generates proxy classes for entity
classes.
classes. Deprecated in favor of using native lazy objects.
- ``orm:run-dql`` Executes arbitrary DQL directly from the command
line.
- ``orm:schema-tool:create`` Processes the schema and either

View File

@@ -112,7 +112,6 @@ of several common elements:
<indexes>
<index name="name_idx" columns="name"/>
<index columns="user_email"/>
</indexes>
<unique-constraints>
@@ -131,7 +130,7 @@ of several common elements:
</id>
<field name="name" column="name" type="string" length="50" nullable="true" unique="true" />
<field name="email" column="user_email" type="string" column-definition="CHAR(32) NOT NULL" />
<field name="email" column="user_email" type="string" index="true" column-definition="CHAR(32) NOT NULL" />
<one-to-one field="address" target-entity="Address" inversed-by="user">
<cascade><cascade-remove /></cascade>
@@ -255,6 +254,8 @@ Optional attributes:
only.
- unique - Should this field contain a unique value across the
table? Defaults to false.
- index - Should an index be created for this column? Defaults to
false.
- nullable - Should this field allow NULL as a value? Defaults to
false.
- insertable - Should this field be inserted? Defaults to true.

View File

@@ -138,12 +138,12 @@ step:
require_once "vendor/autoload.php";
// Create a simple "default" Doctrine ORM configuration for Attributes
$config = ORMSetup::createAttributeMetadataConfiguration(
$config = ORMSetup::createAttributeMetadataConfig( // on PHP < 8.4, use ORMSetup::createAttributeMetadataConfiguration()
paths: [__DIR__ . '/src'],
isDevMode: true,
);
// or if you prefer XML
// $config = ORMSetup::createXMLMetadataConfiguration(
// $config = ORMSetup::createXMLMetadataConfig( // on PHP < 8.4, use ORMSetup::createXMLMetadataConfiguration()
// paths: [__DIR__ . '/config/xml'],
// isDevMode: true,
//);

View File

@@ -243,6 +243,7 @@
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="unique" type="xs:boolean" default="false" />
<xs:attribute name="nullable" type="xs:boolean" default="false" />
<xs:attribute name="index" type="xs:boolean" default="false" />
<xs:attribute name="insertable" type="xs:boolean" default="true" />
<xs:attribute name="updatable" type="xs:boolean" default="true" />
<xs:attribute name="generated" type="orm:generated-type" default="NEVER" />

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
@@ -1351,51 +1333,45 @@ parameters:
path: src/Mapping/Driver/AttributeDriver.php
-
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencedColumnNames\(\)\.$#'
identifier: method.notFound
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\ForeignKeyConstraint'' and ''getReferencedColumn…'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencedTableName\(\)\.$#'
identifier: method.notFound
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\ForeignKeyConstraint'' and ''getReferencedTableN…'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencingColumnNames\(\)\.$#'
identifier: method.notFound
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\ForeignKeyConstraint'' and ''getReferencingColum…'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\Index\:\:getIndexedColumns\(\)\.$#'
identifier: method.notFound
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\Index'' and ''getIndexedColumns'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\Table\:\:getPrimaryKeyConstraint\(\)\.$#'
identifier: method.notFound
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\Table'' and ''getPrimaryKeyConstr…'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Call to method getColumnName\(\) on an unknown class Doctrine\\DBAL\\Schema\\Index\\IndexedColumn\.$#'
identifier: class.notFound
message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Schema\\Index and ''getType'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Call to method getColumnNames\(\) on an unknown class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint\.$#'
identifier: class.notFound
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Call to method toString\(\) on an unknown class Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\.$#'
identifier: class.notFound
count: 5
message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Schema\\Table and ''getPrimaryKeyConstr…'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 2
path: src/Mapping/Driver/DatabaseDriver.php
-
@@ -1404,18 +1380,6 @@ parameters:
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Class Doctrine\\DBAL\\Schema\\Index\\IndexType not found\.$#'
identifier: class.notFound
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint not found\.$#'
identifier: class.notFound
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Instanceof between Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\> and Doctrine\\ORM\\Mapping\\ClassMetadata will always evaluate to true\.$#'
identifier: instanceof.alwaysTrue
@@ -1452,30 +1416,12 @@ parameters:
count: 2
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Parameter \#1 \$array of function sort contains unresolvable type\.$#'
identifier: argument.unresolvableType
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Parameter \#2 \$columnName of method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getFieldNameForColumn\(\) expects string, string\|false given\.$#'
identifier: argument.type
count: 4
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Parameter \$indexedColumn of anonymous function has invalid type Doctrine\\DBAL\\Schema\\Index\\IndexedColumn\.$#'
identifier: class.notFound
count: 1
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Parameter \$name of anonymous function has invalid type Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\.$#'
identifier: class.notFound
count: 5
path: src/Mapping/Driver/DatabaseDriver.php
-
message: '#^Method Doctrine\\ORM\\Mapping\\Driver\\SimplifiedXmlDriver\:\:__construct\(\) has parameter \$fileExtension with no type specified\.$#'
identifier: missingType.parameter
@@ -1578,12 +1524,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
@@ -2016,6 +1986,12 @@ parameters:
count: 1
path: src/Persisters/Collection/OneToManyPersister.php
-
message: '#^Parameter \#1 \$columns of method Doctrine\\DBAL\\Platforms\\AbstractPlatform\:\:getColumnDeclarationListSQL\(\) expects list\<array\{name\: string, type\: Doctrine\\DBAL\\Types\\Type, default\: mixed, notnull\?\: bool, autoincrement\: bool, columnDefinition\: non\-empty\-string\|null, comment\: string, charset\?\: non\-empty\-string\|null, \.\.\.\}\>, array\<string, array\{name\: string, notnull\: true, type\: Doctrine\\DBAL\\Types\\Type\}\> given\.$#'
identifier: argument.type
count: 1
path: src/Persisters/Collection/OneToManyPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\AbstractEntityInheritancePersister\:\:getSelectColumnSQL\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
@@ -2394,12 +2370,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
@@ -2610,6 +2580,12 @@ parameters:
count: 1
path: src/Query/Exec/MultiTableDeleteExecutor.php
-
message: '#^Parameter \#1 \$columns of method Doctrine\\DBAL\\Platforms\\AbstractPlatform\:\:getColumnDeclarationListSQL\(\) expects list\<array\{name\: string, type\: Doctrine\\DBAL\\Types\\Type, default\: mixed, notnull\?\: bool, autoincrement\: bool, columnDefinition\: non\-empty\-string\|null, comment\: string, charset\?\: non\-empty\-string\|null, \.\.\.\}\>, array\<string, array\{name\: string, notnull\: true, type\: Doctrine\\DBAL\\Types\\Type\}\> given\.$#'
identifier: argument.type
count: 1
path: src/Query/Exec/MultiTableDeleteExecutor.php
-
message: '#^Argument of an invalid type list\<string\>\|string supplied for foreach, only iterables are supported\.$#'
identifier: foreach.nonIterable
@@ -2622,6 +2598,12 @@ parameters:
count: 1
path: src/Query/Exec/MultiTableUpdateExecutor.php
-
message: '#^Parameter \#1 \$columns of method Doctrine\\DBAL\\Platforms\\AbstractPlatform\:\:getColumnDeclarationListSQL\(\) expects list\<array\{name\: string, type\: Doctrine\\DBAL\\Types\\Type, default\: mixed, notnull\?\: bool, autoincrement\: bool, columnDefinition\: non\-empty\-string\|null, comment\: string, charset\?\: non\-empty\-string\|null, \.\.\.\}\>, array\<string, array\{name\: string, notnull\: true, type\: Doctrine\\DBAL\\Types\\Type\}\> given\.$#'
identifier: argument.type
count: 1
path: src/Query/Exec/MultiTableUpdateExecutor.php
-
message: '#^Parameter \#3 \$types of method Doctrine\\DBAL\\Connection\:\:executeStatement\(\) expects array\<int\<0, max\>\|string, Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|Doctrine\\DBAL\\Types\\Type\|string\>, list\<Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|Doctrine\\DBAL\\Types\\Type\|int\|string\> given\.$#'
identifier: argument.type
@@ -2868,12 +2850,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
@@ -3127,32 +3103,71 @@ parameters:
path: src/Tools/SchemaTool.php
-
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencedColumnNames\(\)\.$#'
identifier: method.notFound
message: '''
#^Call to deprecated method getColumns\(\) of class Doctrine\\DBAL\\Schema\\Index\:
Use \{@see getIndexedColumns\(\)\} instead\.$#
'''
identifier: method.deprecated
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencedTableName\(\)\.$#'
identifier: method.notFound
message: '''
#^Call to deprecated method getForeignColumns\(\) of class Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:
Use \{@see getReferencedColumnNames\(\)\} instead\.
Returns the names of the referenced table columns
the foreign key constraint is associated with\.$#
'''
identifier: method.deprecated
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:\:getReferencingColumnNames\(\)\.$#'
identifier: method.notFound
message: '''
#^Call to deprecated method getForeignTableName\(\) of class Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:
Use \{@see getReferencedTableName\(\)\} instead\.
Returns the name of the referenced table
the foreign key constraint is associated with\.$#
'''
identifier: method.deprecated
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\Index\:\:getIndexedColumns\(\)\.$#'
identifier: method.notFound
message: '''
#^Call to deprecated method getLocalColumns\(\) of class Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\:
Use \{@see getReferencingColumnNames\(\)\} instead\.$#
'''
identifier: method.deprecated
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Call to an undefined method Doctrine\\DBAL\\Schema\\Table\:\:dropForeignKey\(\)\.$#'
identifier: method.notFound
message: '''
#^Call to deprecated method getPrimaryKey\(\) of class Doctrine\\DBAL\\Schema\\Table\:
Use \{@see getPrimaryKeyConstraint\(\)\} instead\.$#
'''
identifier: method.deprecated
count: 1
path: src/Tools/SchemaTool.php
-
message: '''
#^Call to deprecated method removeForeignKey\(\) of class Doctrine\\DBAL\\Schema\\Table\:
Use \{@link dropForeignKey\(\)\} instead\.$#
'''
identifier: method.deprecated
count: 1
path: src/Tools/SchemaTool.php
-
message: '''
#^Call to deprecated method setPrimaryKey\(\) of class Doctrine\\DBAL\\Schema\\Table\:
Use \{@see addPrimaryKeyConstraint\(\)\} instead\.$#
'''
identifier: method.deprecated
count: 1
path: src/Tools/SchemaTool.php
@@ -3163,32 +3178,14 @@ parameters:
path: src/Tools/SchemaTool.php
-
message: '#^Call to method getColumnName\(\) on an unknown class Doctrine\\DBAL\\Schema\\Index\\IndexedColumn\.$#'
identifier: class.notFound
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Schema\\\\Index'' and ''getIndexedColumns'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Call to method getColumnNames\(\) on an unknown class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint\.$#'
identifier: class.notFound
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Call to method toString\(\) on an unknown class Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\.$#'
identifier: class.notFound
count: 3
path: src/Tools/SchemaTool.php
-
message: '#^Class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint not found\.$#'
identifier: class.notFound
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Method Doctrine\\DBAL\\Schema\\AbstractSchemaManager\<Doctrine\\DBAL\\Platforms\\AbstractPlatform\>\:\:createComparator\(\) invoked with 1 parameter, 0 required\.$#'
identifier: arguments.count
message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Schema\\Table and ''getPrimaryKeyConstr…'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Tools/SchemaTool.php
@@ -3295,15 +3292,51 @@ parameters:
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \$indexedColumn of anonymous function has invalid type Doctrine\\DBAL\\Schema\\Index\\IndexedColumn\.$#'
identifier: class.notFound
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addIndex\(\) expects non\-empty\-list\<string\>, list\<string\> given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \$name of anonymous function has invalid type Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\.$#'
identifier: class.notFound
count: 3
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addUniqueIndex\(\) expects non\-empty\-list\<string\>, array\<string\> given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#1 \$columnNames of method Doctrine\\DBAL\\Schema\\Table\:\:setPrimaryKey\(\) expects non\-empty\-list\<string\>, array\<string\> given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#1 \$value of static method Doctrine\\DBAL\\Schema\\Name\\Identifier\:\:unquoted\(\) expects non\-empty\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#2 \$columnNames of class Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint constructor expects non\-empty\-list\<Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\>, list\<Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName\> given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#2 \$columns of class Doctrine\\DBAL\\Schema\\Index constructor expects non\-empty\-list\<string\>, list\<string\> given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#2 \$localColumnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addForeignKeyConstraint\(\) expects non\-empty\-list\<string\>, list\<string\> given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
message: '#^Parameter \#3 \$foreignColumnNames of method Doctrine\\DBAL\\Schema\\Table\:\:addForeignKeyConstraint\(\) expects non\-empty\-list\<string\>, list\<string\> given\.$#'
identifier: argument.type
count: 1
path: src/Tools/SchemaTool.php
-
@@ -3456,12 +3489,6 @@ parameters:
count: 1
path: src/UnitOfWork.php
-
message: '#^PHPDoc tag @phpstan\-assert\-if\-true for \$obj contains generic interface Doctrine\\ORM\\Proxy\\InternalProxy but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: src/UnitOfWork.php
-
message: '#^Parameter \#2 \$assoc of method Doctrine\\ORM\\PersistentCollection\<\(int\|string\),mixed\>\:\:setOwner\(\) expects Doctrine\\ORM\\Mapping\\AssociationMapping&Doctrine\\ORM\\Mapping\\ToManyAssociationMapping, Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping given\.$#'
identifier: argument.type
@@ -3504,6 +3531,12 @@ parameters:
count: 1
path: src/UnitOfWork.php
-
message: '#^Unable to resolve the template type T in call to method static method Symfony\\Component\\VarExporter\\Hydrator\:\:hydrate\(\)$#'
identifier: argument.templateType
count: 1
path: src/UnitOfWork.php
-
message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\:\:\$name\.$#'
identifier: property.notFound

View File

@@ -34,8 +34,58 @@ parameters:
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\Table::addPrimaryKeyConstraint\(\)\.$~'
path: src/Tools/SchemaTool.php
-
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\ForeignKeyConstraint::get.*\.$~'
identifier: method.notFound
-
message: '~createComparator~'
identifier: arguments.count
-
message: '~UnqualifiedName~'
identifier: class.notFound
-
message: '~IndexedColumn~'
identifier: class.notFound
-
message: '~PrimaryKeyConstraint~'
identifier: class.notFound
-
message: '~IndexType~'
identifier: class.notFound
-
message: '~dropForeignKey~'
identifier: method.notFound
-
message: '~getIndexedColumns~'
identifier: method.notFound
-
message: '~getPrimaryKeyConstraint~'
identifier: method.notFound
-
message: '~PrimaryKeyConstraint~'
identifier: class.notFound
path: src/Tools/SchemaTool.php
-
message: '~^Call to method toString.*UnqualifiedName\.$~'
path: src/Tools/SchemaTool.php
- '~^Class Doctrine\\DBAL\\Platforms\\SQLitePlatform not found\.$~'
-
message: '~sort~'
identifier: argument.unresolvableType
path: src/Mapping/Driver/DatabaseDriver.php
# To be removed in 4.0
-
message: '#Negated boolean expression is always false\.#'

View File

@@ -20,18 +20,6 @@ parameters:
message: '~^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~'
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '~^Call to static method unquoted\(\) on an unknown class Doctrine\\DBAL\\Schema\\Name\\Identifier\.$~'
path: src/Tools/SchemaTool.php
-
message: '~^Instantiated class Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName not found\.$~'
path: src/Tools/SchemaTool.php
-
message: '~^Call to an undefined method Doctrine\\DBAL\\Schema\\Table::addPrimaryKeyConstraint\(\)\.$~'
path: src/Tools/SchemaTool.php
# Compatibility with DBAL 3
# See https://github.com/doctrine/dbal/pull/3480
-
@@ -45,7 +33,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

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Cache\CacheConfiguration;
use Doctrine\ORM\Exception\InvalidEntityRepository;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
@@ -30,6 +31,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.
@@ -61,6 +64,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
*/
public function setProxyDir(string $dir): void
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
$this->attributes['proxyDir'] = $dir;
}
@@ -69,6 +81,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
*/
public function getProxyDir(): string|null
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
return $this->attributes['proxyDir'] ?? null;
}
@@ -79,6 +100,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
*/
public function getAutoGenerateProxyClasses(): int
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
return $this->attributes['autoGenerateProxyClasses'] ?? ProxyFactory::AUTOGENERATE_ALWAYS;
}
@@ -89,6 +119,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
*/
public function setAutoGenerateProxyClasses(bool|int $autoGenerate): void
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
$this->attributes['autoGenerateProxyClasses'] = (int) $autoGenerate;
}
@@ -97,6 +136,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
*/
public function getProxyNamespace(): string|null
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
return $this->attributes['proxyNamespace'] ?? null;
}
@@ -105,6 +153,15 @@ class Configuration extends \Doctrine\DBAL\Configuration
*/
public function setProxyNamespace(string $ns): void
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
$this->attributes['proxyNamespace'] = $ns;
}
@@ -593,8 +650,30 @@ 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 && ! $nativeLazyObjects) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Disabling native lazy objects is deprecated and will be impossible in Doctrine ORM 4.0.',
);
}
if (PHP_VERSION_ID < 80400 && $nativeLazyObjects) {
throw new LogicException('Lazy loading proxies require PHP 8.4 or higher.');
}
$this->attributes['nativeLazyObjects'] = $nativeLazyObjects;
}
/**
* To be deprecated in 3.1.0
* @deprecated lazy ghost objects are always enabled
*
* @return true
*/
@@ -603,7 +682,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
return true;
}
/** To be deprecated in 3.1.0 */
/** @deprecated lazy ghost objects cannot be disabled */
public function setLazyGhostObjectEnabled(bool $flag): void
{
if (! $flag) {
@@ -614,7 +693,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
}
}
/** To be deprecated in 3.1.0 */
/** @deprecated rejecting ID collisions in the identity map cannot be disabled */
public function setRejectIdCollisionInIdentityMap(bool $flag): void
{
if (! $flag) {
@@ -626,7 +705,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
}
/**
* To be deprecated in 3.1.0
* @deprecated rejecting ID collisions in the identity map is always enabled
*
* @return true
*/

View File

@@ -43,7 +43,7 @@ use function method_exists;
*
* $paths = ['/path/to/entity/mapping/files'];
*
* $config = ORMSetup::createAttributeMetadataConfiguration($paths);
* $config = ORMSetup::createAttributeMetadataConfig($paths);
* $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config);
* $entityManager = new EntityManager($connection, $config);
*
@@ -134,12 +134,16 @@ class EntityManager implements EntityManagerInterface
$this->repositoryFactory = $config->getRepositoryFactory();
$this->unitOfWork = new UnitOfWork($this);
$this->proxyFactory = new ProxyFactory(
$this,
$config->getProxyDir(),
$config->getProxyNamespace(),
$config->getAutoGenerateProxyClasses(),
);
if ($config->isNativeLazyObjectsEnabled()) {
$this->proxyFactory = new ProxyFactory($this);
} else {
$this->proxyFactory = new ProxyFactory(
$this,
$config->getProxyDir(),
$config->getProxyNamespace(),
$config->getAutoGenerateProxyClasses(),
);
}
if ($config->isSecondLevelCacheEnabled()) {
$cacheConfig = $config->getSecondLevelCacheConfiguration();

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

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal\UnitOfWork;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* An {@see InsertBatch} represents a set of entities that are safe to be batched
* together in a single query.
*
* These entities are only those that have all fields already assigned, including the
* identifier field(s).
*
* This data structure only exists for internal {@see UnitOfWork} optimisations, and
* should not be relied upon outside the ORM.
*
* @internal
*
* @template TEntity of object
*/
final class InsertBatch
{
/**
* @param ClassMetadata<TEntity> $class
* @param non-empty-list<TEntity> $entities
*/
public function __construct(
public readonly ClassMetadata $class,
public array $entities,
) {
}
/**
* Note: Code in here is procedural/ugly due to it being in a hot path of the {@see UnitOfWork}
*
* This method will batch the given entity set by type, preserving their order. For example,
* given an input [A1, A2, A3, B1, B2, A4, A5], it will create an [[A1, A2, A3], [B1, B2], [A4, A5]] batch.
*
* Entities for which the identifier needs to be generated or fetched by a sequence are put as single
* items in a batch of their own, since it is unsafe to batch-insert them.
*
* @param list<TEntities> $entities
*
* @return list<self<TEntities>>
*
* @template TEntities of object
*/
public static function batchByEntityType(
EntityManagerInterface $entityManager,
array $entities,
): array {
$currentClass = null;
$batches = [];
$batchIndex = -1;
foreach ($entities as $entity) {
$entityClass = $entityManager->getClassMetadata($entity::class);
if (
$currentClass?->name !== $entityClass->name
|| ! $entityClass->idGenerator instanceof AssignedGenerator
) {
$currentClass = $entityClass;
$batches[] = new InsertBatch($entityClass, [$entity]);
$batchIndex += 1;
continue;
}
$batches[$batchIndex]->entities[] = $entity;
}
return $batches;
}
}

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

@@ -64,6 +64,18 @@ class FieldBuilder
return $this;
}
/**
* Sets indexed.
*
* @return $this
*/
public function index(bool $flag = true): static
{
$this->mapping['index'] = $flag;
return $this;
}
/**
* Sets column name.
*

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.
@@ -399,13 +400,9 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
public DiscriminatorColumnMapping|null $discriminatorColumn = null;
/**
* READ-ONLY: The primary table definition. The definition is an array with the
* following entries:
* READ-ONLY: The primary table definition.
*
* name => <tableName>
* schema => <schemaName>
* indexes => array
* uniqueConstraints => array
* "quoted" indicates whether the table name is quoted (with backticks) or not
*
* @var mixed[]
* @phpstan-var array{
@@ -543,9 +540,14 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
/**
* The ReflectionProperty instances of the mapped class.
*
* @var array<string, ReflectionProperty|null>
* @deprecated Use $propertyAccessors instead.
*
* @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,24 +571,43 @@ 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>
* @deprecated Use getPropertyAccessors() instead.
*
* @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.
*
* @deprecated Use getPropertyAccessor() instead.
*/
public function getReflectionProperty(string $name): ReflectionProperty|null
{
return $this->reflFields[$name];
}
public function getPropertyAccessor(string $name): PropertyAccessor|null
{
return $this->propertyAccessors[$name] ?? null;
}
/**
* Gets the ReflectionProperty for the single identifier field.
* @deprecated Use getPropertyAccessor() instead.
*
* @throws BadMethodCallException If the class has a composite identifier.
*/
@@ -599,6 +620,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 +644,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 +655,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 +674,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 +683,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 +691,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);
}
/**
@@ -793,77 +824,76 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
public function wakeupReflection(ReflectionService $reflService): void
{
// Restore ReflectionClass and properties
$this->reflClass = $reflService->getClass($this->name);
$this->reflClass = $reflService->getClass($this->name);
/** @phpstan-ignore property.deprecated */
$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);
}
}
@@ -1033,6 +1063,13 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
return $mapping !== false && isset($mapping->nullable) && $mapping->nullable;
}
public function isIndexed(string $fieldName): bool
{
$mapping = $this->getFieldMapping($fieldName);
return isset($mapping->index) && $mapping->index;
}
/**
* Gets a column name for a field name.
* If the column name for the field cannot be found, the given field name
@@ -2659,26 +2696,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
@@ -64,7 +66,9 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
public function setEntityManager(EntityManagerInterface $em): void
{
parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
if (! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
}
$this->em = $em;
}
@@ -440,8 +444,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 +703,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

@@ -31,6 +31,7 @@ final class Column implements MappingAttribute
public readonly array $options = [],
public readonly string|null $columnDefinition = null,
public readonly string|null $generated = null,
public readonly bool $index = false,
) {
}
}

View File

@@ -10,8 +10,11 @@ use Doctrine\ORM\Internal\SQLResultCasing;
use function array_map;
use function array_merge;
use function assert;
use function explode;
use function implode;
use function is_numeric;
use function preg_replace;
use function sprintf;
use function substr;
/**
@@ -38,7 +41,13 @@ class DefaultQuoteStrategy implements QuoteStrategy
$tableName = $class->table['name'];
if (! empty($class->table['schema'])) {
$tableName = $class->table['schema'] . '.' . $class->table['name'];
return isset($class->table['quoted'])
? sprintf(
'%s.%s',
$platform->quoteSingleIdentifier($class->table['schema']),
$platform->quoteSingleIdentifier($tableName),
)
: $class->table['schema'] . '.' . $class->table['name'];
}
return isset($class->table['quoted'])
@@ -52,7 +61,10 @@ class DefaultQuoteStrategy implements QuoteStrategy
public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string
{
return isset($definition['quoted'])
? $platform->quoteSingleIdentifier($definition['sequenceName'])
? implode('.', array_map(
static fn (string $part) => $platform->quoteSingleIdentifier($part),
explode('.', $definition['sequenceName']),
))
: $definition['sequenceName'];
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use BackedEnum;
use BcMath\Number;
use DateInterval;
use DateTime;
use DateTimeImmutable;
@@ -40,7 +41,12 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
/** @param array<class-string|ScalarName, class-string<Type>|string> $typedFieldMappings */
public function __construct(array $typedFieldMappings = [])
{
$this->typedFieldMappings = array_merge(self::DEFAULT_TYPED_FIELD_MAPPINGS, $typedFieldMappings);
$defaultMappings = self::DEFAULT_TYPED_FIELD_MAPPINGS;
if (defined(Types::class . '::NUMBER')) { // DBAL 4.3+
$defaultMappings[Number::class] = Types::NUMBER;
}
$this->typedFieldMappings = array_merge($defaultMappings, $typedFieldMappings);
}
/**

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,
@@ -709,6 +710,7 @@ class AttributeDriver implements MappingDriver
* length: int,
* unique: bool,
* nullable: bool,
* index: bool,
* precision: int,
* enumType?: class-string,
* options?: mixed[],
@@ -725,6 +727,7 @@ class AttributeDriver implements MappingDriver
'length' => $column->length,
'unique' => $column->unique,
'nullable' => $column->nullable,
'index' => $column->index,
'precision' => $column->precision,
];

View File

@@ -754,6 +754,7 @@ class XmlDriver extends FileDriver
* scale?: int,
* unique?: bool,
* nullable?: bool,
* index?: bool,
* notInsertable?: bool,
* notUpdatable?: bool,
* enumType?: string,
@@ -792,6 +793,10 @@ class XmlDriver extends FileDriver
$mapping['unique'] = $this->evaluateBoolean($fieldMapping['unique']);
}
if (isset($fieldMapping['index'])) {
$mapping['index'] = $this->evaluateBoolean($fieldMapping['index']);
}
if (isset($fieldMapping['nullable'])) {
$mapping['nullable'] = $this->evaluateBoolean($fieldMapping['nullable']);
}

View File

@@ -42,6 +42,8 @@ final class FieldMapping implements ArrayAccess
public int|null $scale = null;
/** Whether a unique constraint should be generated for the column. */
public bool|null $unique = null;
/** Whether an index should be generated for the column. */
public bool|null $index = null;
/**
* @var class-string|null This is set when the field is inherited by this
* class from another (inheritance) parent <em>entity</em> class. The value
@@ -54,6 +56,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;
@@ -92,6 +95,7 @@ final class FieldMapping implements ArrayAccess
* length?: int|null,
* id?: bool|null,
* nullable?: bool|null,
* index?: bool|null,
* notInsertable?: bool|null,
* notUpdatable?: bool|null,
* columnDefinition?: string|null,
@@ -101,7 +105,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,9 +9,10 @@ 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 bool|null $nullable = null,
public readonly mixed $onDelete = null,
public readonly string|null $columnDefinition = null,
public readonly string|null $fieldName = 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(),
);
}
}
@@ -119,6 +127,8 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
$mapping->joinTableColumns = [];
foreach ($mapping->joinTable->joinColumns as $joinColumn) {
$joinColumn->nullable = false;
if (empty($joinColumn->referencedColumnName)) {
$joinColumn->referencedColumnName = $namingStrategy->referenceColumnName();
}
@@ -142,6 +152,8 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
}
foreach ($mapping->joinTable->inverseJoinColumns as $inverseJoinColumn) {
$inverseJoinColumn->nullable = false;
if (empty($inverseJoinColumn->referencedColumnName)) {
$inverseJoinColumn->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();
}
}
}
@@ -126,6 +130,12 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
$uniqueConstraintColumns = [];
foreach ($mapping->joinColumns as $joinColumn) {
if ($mapping->id) {
$joinColumn->nullable = false;
} elseif ($joinColumn->nullable === null) {
$joinColumn->nullable = true;
}
if ($mapping->isOneToOne() && ! $isInheritanceTypeSingleTable) {
if (count($mapping->joinColumns) === 1) {
if (empty($mapping->id)) {

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\Driver\XmlDriver;
use Psr\Cache\CacheItemPoolInterface;
@@ -20,6 +21,8 @@ use function extension_loaded;
use function md5;
use function sys_get_temp_dir;
use const PHP_VERSION_ID;
final class ORMSetup
{
/**
@@ -33,12 +36,39 @@ final class ORMSetup
string|null $proxyDir = null,
CacheItemPoolInterface|null $cache = null,
): Configuration {
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'%s is deprecated in favor of %s, and will be removed in 4.0.',
__METHOD__,
self::class . '::createAttributeMetadataConfig()',
);
}
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl(new AttributeDriver($paths));
return $config;
}
/**
* Creates a configuration with an attribute metadata driver.
*
* @param string[] $paths
*/
public static function createAttributeMetadataConfig(
array $paths,
bool $isDevMode = false,
string|null $cacheNamespaceSeed = null,
CacheItemPoolInterface|null $cache = null,
): Configuration {
$config = self::createConfig($isDevMode, $cacheNamespaceSeed, $cache);
$config->setMetadataDriverImpl(new AttributeDriver($paths));
return $config;
}
/**
* Creates a configuration with an XML metadata driver.
*
@@ -51,12 +81,44 @@ final class ORMSetup
CacheItemPoolInterface|null $cache = null,
bool $isXsdValidationEnabled = true,
): Configuration {
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'%s is deprecated in favor of %s, and will be removed in 4.0.',
__METHOD__,
self::class . '::createXMLMetadataConfig()',
);
}
$config = self::createConfiguration($isDevMode, $proxyDir, $cache);
$config->setMetadataDriverImpl(new XmlDriver($paths, XmlDriver::DEFAULT_FILE_EXTENSION, $isXsdValidationEnabled));
return $config;
}
/**
* Creates a configuration with an XML metadata driver.
*
* @param string[] $paths
*/
public static function createXMLMetadataConfig(
array $paths,
bool $isDevMode = false,
string|null $cacheNamespaceSeed = null,
CacheItemPoolInterface|null $cache = null,
bool $isXsdValidationEnabled = true,
): Configuration {
$config = self::createConfig($isDevMode, $cacheNamespaceSeed, $cache);
$config->setMetadataDriverImpl(new XmlDriver(
$paths,
XmlDriver::DEFAULT_FILE_EXTENSION,
$isXsdValidationEnabled,
));
return $config;
}
/**
* Creates a configuration without a metadata driver.
*/
@@ -65,6 +127,16 @@ final class ORMSetup
string|null $proxyDir = null,
CacheItemPoolInterface|null $cache = null,
): Configuration {
if (PHP_VERSION_ID >= 80400 && $proxyDir !== null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'%s is deprecated in favor of %s, and will be removed in 4.0.',
__METHOD__,
self::class . '::createConfig()',
);
}
$proxyDir = $proxyDir ?: sys_get_temp_dir();
$cache = self::createCacheInstance($isDevMode, $proxyDir, $cache);
@@ -81,9 +153,23 @@ final class ORMSetup
return $config;
}
public static function createConfig(
bool $isDevMode = false,
string|null $cacheNamespaceSeed = null,
CacheItemPoolInterface|null $cache = null,
): Configuration {
$cache = self::createCacheInstance($isDevMode, $cacheNamespaceSeed, $cache);
$config = new Configuration();
$config->setMetadataCache($cache);
$config->setQueryCache($cache);
$config->setResultCache($cache);
return $config;
}
private static function createCacheInstance(
bool $isDevMode,
string $proxyDir,
string|null $cacheNamespaceSeed,
CacheItemPoolInterface|null $cache,
): CacheItemPoolInterface {
if ($cache !== null) {
@@ -101,7 +187,7 @@ final class ORMSetup
return new ArrayAdapter();
}
$namespace = 'dc2_' . md5($proxyDir);
$namespace = 'dc2_' . md5($cacheNamespaceSeed ?? 'default');
if (extension_loaded('apcu') && apcu_enabled()) {
return new ApcuAdapter($namespace);

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

@@ -153,12 +153,6 @@ class BasicEntityPersister implements EntityPersister
*/
protected array $quotedColumns = [];
/**
* The INSERT SQL statement used for entities handled by this persister.
* This SQL is only generated once per request, if at all.
*/
private string|null $insertSql = null;
/**
* The quote strategy.
*/
@@ -273,8 +267,8 @@ class BasicEntityPersister implements EntityPersister
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
// Unset this queued insert, so that the prepareUpdateData() method knows right away
// (for the next entity already) that the current entity has been written to the database
// Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() method)
// knows right away (for the next entity already) that the current entity has been written to the database
// and no extra updates need to be scheduled to refer to it.
//
// In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
@@ -480,7 +474,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:
@@ -693,11 +687,11 @@ class BasicEntityPersister implements EntityPersister
$targetColumn = $joinColumn->referencedColumnName;
$quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
$this->quotedColumns[$sourceColumn] = $quotedColumn;
$this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
$result[$owningTable][$sourceColumn] = $newValId
? $newValId[$targetClass->getFieldForColumn($targetColumn)]
: null;
$this->quotedColumns[$sourceColumn] = $quotedColumn;
$this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
$newValue = $newValId ? $newValId[$targetClass->getFieldForColumn($targetColumn)] : null;
$result[$owningTable][$sourceColumn] = $newValue instanceof BackedEnum ? $newValue->value : $newValue;
}
}
@@ -791,7 +785,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;
@@ -838,7 +832,7 @@ class BasicEntityPersister implements EntityPersister
}
} else {
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
$sourceClass->propertyAccessors[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
}
}
@@ -1055,7 +1049,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);
@@ -1066,7 +1060,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;
@@ -1418,22 +1412,17 @@ class BasicEntityPersister implements EntityPersister
public function getInsertSQL(): string
{
if ($this->insertSql !== null) {
return $this->insertSql;
}
$columns = $this->getInsertColumnList();
$tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
if (empty($columns)) {
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
$this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
if ($columns === []) {
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
return $this->insertSql;
return $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
}
$values = [];
$columns = array_unique($columns);
$placeholders = [];
$columns = array_unique($columns);
foreach ($columns as $column) {
$placeholder = '?';
@@ -1447,15 +1436,13 @@ class BasicEntityPersister implements EntityPersister
$placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
}
$values[] = $placeholder;
$placeholders[] = $placeholder;
}
$columns = implode(', ', $columns);
$values = implode(', ', $values);
$columns = implode(', ', $columns);
$placeholders = implode(', ', $placeholders);
$this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
return $this->insertSql;
return sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $placeholders);
}
/**
@@ -1470,7 +1457,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;
}
@@ -1824,7 +1811,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);
@@ -1842,7 +1829,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[] = [
@@ -1964,6 +1951,7 @@ class BasicEntityPersister implements EntityPersister
ParameterType::STRING => ArrayParameterType::STRING,
ParameterType::INTEGER => ArrayParameterType::INTEGER,
ParameterType::ASCII => ArrayParameterType::ASCII,
ParameterType::BINARY => ArrayParameterType::BINARY,
};
}

View File

@@ -134,7 +134,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
// Execute all inserts. For each entity:
// 1) Insert on root table
// 2) Insert on sub tables
foreach ($this->queuedInserts as $entity) {
foreach ($this->queuedInserts as $key => $entity) {
$insertData = $this->prepareInsertData($entity);
// Execute insert on root table
@@ -179,9 +179,16 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
if ($this->class->requiresFetchAfterChange) {
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
}
$this->queuedInserts = [];
// Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() method)
// knows right away (for the next entity already) that the current entity has been written to the database
// and no extra updates need to be scheduled to refer to it.
//
// In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
// from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they
// were given to our addInsert() method.
unset($this->queuedInserts[$key]);
}
}
public function update(object $entity): void
@@ -460,7 +467,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

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Proxy;
use Closure;
use Doctrine\Deprecations\Deprecation;
use function file_exists;
use function ltrim;
@@ -15,6 +16,7 @@ use function strlen;
use function substr;
use const DIRECTORY_SEPARATOR;
use const PHP_VERSION_ID;
/**
* Special Autoloader for Proxy classes, which are not PSR-0 compliant.
@@ -34,6 +36,15 @@ final class Autoloader
*/
public static function resolveFile(string $proxyDir, string $proxyNamespace, string $className): string
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Class "%s" is deprecated. Use native lazy objects instead.',
self::class,
);
}
if (! str_starts_with($className, $proxyNamespace)) {
throw new NotAProxyClass($className, $proxyNamespace);
}
@@ -59,6 +70,15 @@ final class Autoloader
string $proxyNamespace,
Closure|null $notFoundCallback = null,
): Closure {
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Class "%s" is deprecated. Use native lazy objects instead.',
self::class,
);
}
$proxyNamespace = ltrim($proxyNamespace, '\\');
$autoloader = /** @param class-string $className */ static function (string $className) use ($proxyDir, $proxyNamespace, $notFoundCallback): void {

View File

@@ -4,12 +4,15 @@ declare(strict_types=1);
namespace Doctrine\ORM\Proxy;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Persistence\Mapping\ProxyClassNameResolver;
use Doctrine\Persistence\Proxy;
use function strrpos;
use function substr;
use const PHP_VERSION_ID;
/**
* Class-related functionality for objects that might or not be proxy objects
* at the moment.
@@ -18,6 +21,15 @@ final class DefaultProxyClassNameResolver implements ProxyClassNameResolver
{
public function resolveClassName(string $className): string
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Class "%s" is deprecated. Use native lazy objects instead.',
self::class,
);
}
$pos = strrpos($className, '\\' . Proxy::MARKER . '\\');
if ($pos === false) {
@@ -30,6 +42,15 @@ final class DefaultProxyClassNameResolver implements ProxyClassNameResolver
/** @return class-string */
public static function getClass(object $object): string
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Class "%s" is deprecated. Use native lazy objects instead.',
self::class,
);
}
return (new self())->resolveClassName($object::class);
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Proxy;
use Closure;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\ORMInvalidArgumentException;
@@ -13,20 +14,24 @@ 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;
use function filemtime;
use function func_num_args;
use function is_bool;
use function is_dir;
use function is_int;
@@ -37,6 +42,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 +51,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 +134,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,22 +148,50 @@ 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) {
throw ORMInvalidArgumentException::proxyDirectoryRequired();
}
if (! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Not enabling native lazy objects is deprecated and will be impossible in Doctrine ORM 4.0.',
);
}
if (! $proxyNs) {
throw ORMInvalidArgumentException::proxyNamespaceRequired();
if (! $proxyDir) {
throw ORMInvalidArgumentException::proxyDirectoryRequired();
}
if (! $proxyNs) {
throw ORMInvalidArgumentException::proxyNamespaceRequired();
}
} elseif (PHP_VERSION_ID >= 80400 && func_num_args() > 1) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Passing more than just the EntityManager to the %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
if (is_int($autoGenerate) ? $autoGenerate < 0 || $autoGenerate > 4 : ! is_bool($autoGenerate)) {
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 +201,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 +247,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 +301,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 +331,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 +356,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 {

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}
*/
@@ -1420,7 +1464,7 @@ final class Parser
assert($this->lexer->lookahead !== null);
$expr = match (true) {
$this->isMathOperator($peek) => $this->SimpleArithmeticExpression(),
$this->isMathOperator($peek) || $this->isMathOperator($glimpse) => $this->SimpleArithmeticExpression(),
$glimpse !== null && $glimpse->type === TokenType::T_DOT => $this->SingleValuedPathExpression(),
$this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(),
$this->lexer->lookahead->type === TokenType::T_CASE => $this->CaseExpression(),
@@ -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

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Tools\Console\Command;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Tools\Console\MetadataFilter;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
@@ -19,6 +20,8 @@ use function mkdir;
use function realpath;
use function sprintf;
use const PHP_VERSION_ID;
/**
* Command to (re)generate the proxy classes used by doctrine.
*
@@ -39,6 +42,14 @@ class GenerateProxiesCommand extends AbstractEntityManagerCommand
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Generating proxies is deprecated and will be impossible in Doctrine ORM 4.0.',
);
}
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$em = $this->getEntityManager($input);

View File

@@ -10,6 +10,8 @@ use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\FieldMapping;
use Doctrine\Persistence\Mapping\MappingException;
use InvalidArgumentException;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -19,6 +21,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
use function array_filter;
use function array_map;
use function array_merge;
use function array_values;
use function count;
use function current;
use function get_debug_type;
@@ -32,6 +35,7 @@ use function preg_match;
use function preg_quote;
use function print_r;
use function sprintf;
use function str_replace;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
@@ -73,6 +77,20 @@ EOT);
return 0;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('entityName')) {
$entityManager = $this->getEntityManager($input);
$entities = array_map(
static fn (string $fqcn) => str_replace('\\', '\\\\', $fqcn),
$this->getMappedEntities($entityManager),
);
$suggestions->suggestValues(array_values($entities));
}
}
/**
* Display all the mapping information for a single Entity.
*
@@ -97,7 +115,7 @@ EOT);
$this->formatField('Embedded class?', $metadata->isEmbeddedClass),
$this->formatField('Parent classes', $metadata->parentClasses),
$this->formatField('Sub classes', $metadata->subClasses),
$this->formatField('Embedded classes', $metadata->subClasses),
$this->formatField('Embedded classes', $metadata->embeddedClasses),
$this->formatField('Identifier', $metadata->identifier),
$this->formatField('Inheritance type', $metadata->inheritanceType),
$this->formatField('Discriminator column', $metadata->discriminatorColumn),

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

@@ -505,6 +505,11 @@ class SchemaTool
if ($isUnique) {
$table->addUniqueIndex([$columnName]);
}
$isIndex = $mapping->index ?? false;
if ($isIndex) {
$table->addIndex([$columnName]);
}
}
/**
@@ -718,6 +723,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;
@@ -154,7 +153,7 @@ class SchemaValidator
$ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the inverse side of a ' .
'bi-directional relationship, but the specified mappedBy association on the target-entity ' .
$assoc->targetEntity . '#' . $assoc->mappedBy . ' does not contain the required ' .
"'inversedBy=\"" . $fieldName . "\"' attribute.";
"'inversedBy: \"" . $fieldName . "\"' attribute.";
} elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy !== $fieldName) {
$ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' .
$assoc->targetEntity . '#' . $assoc->mappedBy . ' are ' .
@@ -175,7 +174,7 @@ class SchemaValidator
$ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the owning side of a ' .
'bi-directional relationship, but the specified inversedBy association on the target-entity ' .
$assoc->targetEntity . '#' . $assoc->inversedBy . ' does not contain the required ' .
"'mappedBy=\"" . $fieldName . "\"' attribute.";
"'mappedBy: \"" . $fieldName . "\"' attribute.";
} elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->mappedBy !== $fieldName) {
$ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' .
$assoc->targetEntity . '#' . $assoc->inversedBy . ' are ' .
@@ -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

@@ -31,6 +31,7 @@ use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Internal\HydrationCompleteHandler;
use Doctrine\ORM\Internal\StronglyConnectedComponents;
use Doctrine\ORM\Internal\TopologicalSort;
use Doctrine\ORM\Internal\UnitOfWork\InsertBatch;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
@@ -586,7 +587,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 +707,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 +746,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;
}
@@ -933,7 +934,9 @@ class UnitOfWork implements PropertyChangedListener
$this->entityStates[$oid] = self::STATE_MANAGED;
$this->scheduleForInsert($entity);
if (! isset($this->entityInsertions[$oid])) {
$this->scheduleForInsert($entity);
}
}
/** @param mixed[] $idValue */
@@ -981,7 +984,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)
@@ -1037,30 +1040,36 @@ class UnitOfWork implements PropertyChangedListener
*/
private function executeInserts(): void
{
$entities = $this->computeInsertExecutionOrder();
$batchedByType = InsertBatch::batchByEntityType($this->em, $this->computeInsertExecutionOrder());
$eventsToDispatch = [];
foreach ($entities as $entity) {
$oid = spl_object_id($entity);
$class = $this->em->getClassMetadata($entity::class);
foreach ($batchedByType as $batch) {
$class = $batch->class;
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
$persister = $this->getEntityPersister($class->name);
$persister->addInsert($entity);
foreach ($batch->entities as $entity) {
$oid = spl_object_id($entity);
unset($this->entityInsertions[$oid]);
$persister->addInsert($entity);
unset($this->entityInsertions[$oid]);
}
$persister->executeInserts();
if (! isset($this->entityIdentifiers[$oid])) {
//entity was not added to identity map because some identifiers are foreign keys to new entities.
//add it now
$this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
}
foreach ($batch->entities as $entity) {
$oid = spl_object_id($entity);
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
if (! isset($this->entityIdentifiers[$oid])) {
//entity was not added to identity map because some identifiers are foreign keys to new entities.
//add it now
$this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
}
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
$eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
$eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
}
}
}
@@ -1167,7 +1176,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 +2038,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 +2079,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 +2125,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 +2188,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 +2244,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,9 +2388,13 @@ 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());
Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
}
} else {
if (
! isset($hints[Query::HINT_REFRESH])
@@ -2404,7 +2417,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 +2447,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 +2493,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 +2559,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 +2579,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 +2590,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 +2670,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 +2692,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,16 +3049,23 @@ 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);
}
}
/**
* Tests if a value is an uninitialized entity.
*
* @phpstan-assert-if-true InternalProxy $obj
*/
/** Tests if a value is an uninitialized entity. */
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 +3264,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,238 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\Tests\Models\Company\CompanyEmployee;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function count;
/**
* Functional tests for ordering with arithmetic expression.
*/
#[Group('GH8011')]
class GH8011Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
$this->useModelSet('company');
parent::setUp();
$this->generateFixture();
}
private function skipIfPostgres(string $test): void
{
$platform = $this->_em->getConnection()->getDatabasePlatform();
if ($platform instanceof PostgreSQLPlatform) {
self::markTestSkipped(
'The ' . $test . ' test does not work on postgresql (see https://github.com/doctrine/orm/pull/8012).',
);
}
}
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpression(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY p.id + p.id ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndSingleValuedPathExpression(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY 1 + p.id ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndSingleValuedPathExpression2(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((1 + p.id)) ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpressionAndLiteral(): void
{
$dql = 'SELECT p ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY p.id + 1 ASC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Benjamin E.', $result[0]->getName());
$this->assertEquals('Guilherme B.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndLiteral(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY s + 1 DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndLiteral2(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((s + 1)) DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariable(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY 1 + s DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariable2(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((1 + s)) DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndSingleValuedPathExpression(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY s + p.id DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithResultVariableAndSingleValuedPathExpression2(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY ((s + p.id)) DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithSingleValuedPathExpressionAndResultVariable(): void
{
$this->skipIfPostgres(__FUNCTION__);
$dql = 'SELECT p, p.salary AS HIDDEN s ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY p.id + s DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function testOrderWithArithmeticExpressionWithLiteralAndResultVariableUsingHiddenResultVariable(): void
{
$dql = 'SELECT p, 1 + p.salary AS HIDDEN _order ' .
'FROM Doctrine\Tests\Models\Company\CompanyEmployee p ' .
'ORDER BY _order DESC';
/** @var CompanyEmployee[] $result */
$result = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($result));
$this->assertEquals('Guilherme B.', $result[0]->getName());
$this->assertEquals('Benjamin E.', $result[1]->getName());
}
public function generateFixture(): void
{
$person1 = new CompanyEmployee();
$person1->setName('Benjamin E.');
$person1->setDepartment('IT');
$person1->setSalary(200000);
$person2 = new CompanyEmployee();
$person2->setName('Guilherme B.');
$person2->setDepartment('IT2');
$person2->setSalary(400000);
$this->_em->persist($person1);
$this->_em->persist($person2);
$this->_em->flush();
$this->_em->clear();
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH12063 extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(GH12063Association::class, GH12063Entity::class);
}
public function testLoadedAssociationWithBackedEnum(): void
{
$association = new GH12063Association();
$association->code = GH12063Code::One;
$this->_em->persist($association);
$this->_em->flush();
$this->_em->clear();
$entity = new GH12063Entity();
$entity->association = $this->_em->find(GH12063Association::class, GH12063Code::One);
$this->_em->persist($entity);
$this->_em->flush();
$this->assertNotNull($entity->id);
}
public function testProxyAssociationWithBackedEnum(): void
{
$association = new GH12063Association();
$association->code = GH12063Code::Two;
$this->_em->persist($association);
$this->_em->flush();
$this->_em->clear();
$entity = new GH12063Entity();
$entity->association = $this->_em->getReference(GH12063Association::class, GH12063Code::Two);
$this->_em->persist($entity);
$this->_em->flush();
$this->assertNotNull($entity->id);
}
}
enum GH12063Code: string
{
case One = 'one';
case Two = 'two';
}
#[Entity]
class GH12063Association
{
#[Id]
#[Column]
public GH12063Code $code;
}
#[Entity]
class GH12063Entity
{
#[Id]
#[Column]
#[GeneratedValue]
public int|null $id = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(referencedColumnName: 'code')]
public GH12063Association $association;
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Performance\LazyLoading;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Proxy\InternalProxy as Proxy;
use Doctrine\Performance\EntityManagerFactory;
use Doctrine\Performance\Mock\NonProxyLoadingEntityManager;
@@ -25,9 +26,11 @@ final class ProxyInitializationTimeBench
/** @var Proxy[] */
private array|null $initializedEmployees = null;
private EntityManager $em;
public function init(): void
{
$proxyFactory = (new NonProxyLoadingEntityManager(EntityManagerFactory::getEntityManager([])))
$proxyFactory = (new NonProxyLoadingEntityManager($this->em = EntityManagerFactory::getEntityManager([])))
->getProxyFactory();
for ($i = 0; $i < 10000; ++$i) {
@@ -36,36 +39,36 @@ final class ProxyInitializationTimeBench
$this->initializedUsers[$i] = $proxyFactory->getProxy(CmsUser::class, ['id' => $i]);
$this->initializedEmployees[$i] = $proxyFactory->getProxy(CmsEmployee::class, ['id' => $i]);
$this->initializedUsers[$i]->__load();
$this->initializedEmployees[$i]->__load();
$this->em->getUnitOfWork()->initializeObject($this->initializedUsers[$i]);
$this->em->getUnitOfWork()->initializeObject($this->initializedEmployees[$i]);
}
}
public function benchCmsUserInitialization(): void
{
foreach ($this->cmsUsers as $proxy) {
$proxy->__load();
$this->em->getUnitOfWork()->initializeObject($proxy);
}
}
public function benchCmsEmployeeInitialization(): void
{
foreach ($this->cmsEmployees as $proxy) {
$proxy->__load();
$this->em->getUnitOfWork()->initializeObject($proxy);
}
}
public function benchInitializationOfAlreadyInitializedCmsUsers(): void
{
foreach ($this->initializedUsers as $proxy) {
$proxy->__load();
$this->em->getUnitOfWork()->initializeObject($proxy);
}
}
public function benchInitializationOfAlreadyInitializedCmsEmployees(): void
{
foreach ($this->initializedEmployees as $proxy) {
$proxy->__load();
$this->em->getUnitOfWork()->initializeObject($proxy);
}
}
}

View File

@@ -13,6 +13,8 @@ use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
*/
class EntityPersisterMock extends BasicEntityPersister
{
/** @var int<0, max> */
private int $countOfExecuteInsertCalls = 0;
private array $inserts = [];
private array $updates = [];
private array $deletes = [];
@@ -40,6 +42,8 @@ class EntityPersisterMock extends BasicEntityPersister
public function executeInserts(): void
{
$this->countOfExecuteInsertCalls += 1;
foreach ($this->postInsertIds as $item) {
$this->em->getUnitOfWork()->assignPostInsertId($item['entity'], $item['generatedId']);
}
@@ -86,6 +90,7 @@ class EntityPersisterMock extends BasicEntityPersister
public function reset(): void
{
$this->countOfExecuteInsertCalls = 0;
$this->existsCalled = false;
$this->identityColumnValueCounter = 0;
$this->inserts = [];
@@ -97,4 +102,10 @@ class EntityPersisterMock extends BasicEntityPersister
{
return $this->existsCalled;
}
/** @return int<0, max> */
public function countOfExecuteInsertCalls(): int
{
return $this->countOfExecuteInsertCalls;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\BinaryPrimaryKey;
use LogicException;
use function bin2hex;
use function hex2bin;
use function random_bytes;
class BinaryId
{
public const LENGTH = 6;
private string $hexId;
private function __construct(string $data)
{
$this->hexId = $data;
}
public static function new(): self
{
return new self(bin2hex(random_bytes(self::LENGTH)));
}
public static function fromBytes(string $value): self
{
return new self(bin2hex($value));
}
public function getBytes(): string
{
$binary = hex2bin($this->hexId);
if ($binary === false) {
throw new LogicException('Cannot convert hex to binary: ' . $this->hexId);
}
return $binary;
}
public function __toString(): string
{
return $this->getBytes();
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\BinaryPrimaryKey;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Doctrine\Tests\Mocks\CompatibilityType;
use LogicException;
final class BinaryIdType extends Type
{
use CompatibilityType;
public const NAME = 'binary_id';
public function convertToPHPValue(
mixed $value,
AbstractPlatform $platform,
): BinaryId|null {
if ($value === null) {
return null;
}
if ($value instanceof BinaryId) {
return $value;
}
return BinaryId::fromBytes($value);
}
public function convertToDatabaseValue(
mixed $value,
AbstractPlatform $platform,
): string|null {
if ($value === null) {
return null;
} elseif ($value instanceof BinaryId) {
return $value->getBytes();
} else {
throw new LogicException('Unexpected value: ' . $value);
}
}
public function getSQLDeclaration(
array $column,
AbstractPlatform $platform,
): string {
return $platform->getBinaryTypeDeclarationSQL([
'length' => BinaryId::LENGTH,
'fixed' => true,
]);
}
private function doGetBindingType(): ParameterType|int
{
return ParameterType::BINARY;
}
public function getName(): string
{
return self::NAME;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\BinaryPrimaryKey;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
#[Entity]
class Category
{
#[Id]
#[Column(type: BinaryIdType::NAME, nullable: false)]
private BinaryId $id;
#[Column]
private string $name;
#[ManyToOne(targetEntity: self::class, inversedBy: 'children')]
private self|null $parent;
/** @var Collection<int, Category> */
#[OneToMany(targetEntity: self::class, mappedBy: 'parent')]
private Collection $children;
public function __construct(
string $name,
self|null $parent = null,
) {
$this->id = BinaryId::new();
$this->name = $name;
$this->parent = $parent;
$this->children = new ArrayCollection();
$parent?->addChild($this);
}
public function getId(): BinaryId
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function getParent(): self|null
{
return $this->parent;
}
/** @return ReadableCollection<int, Category> */
public function getChildren(): ReadableCollection
{
return $this->children;
}
/** @internal */
public function addChild(self $category): void
{
if (! $this->children->contains($category)) {
$this->children->add($category);
}
}
}

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

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\Models\TypedProperties;
use BcMath\Number;
use DateInterval;
use DateTime;
use DateTimeImmutable;
@@ -54,6 +55,9 @@ class UserTyped
#[ORM\Embedded]
public Contact|null $contact = null;
#[ORM\Column(precision: 5, scale: 2)]
public Number|null $bodyHeight = null;
public static function loadMetadata(ClassMetadata $metadata): void
{
$metadata->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_NONE);

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Cache\CacheConfiguration;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityRepository;
@@ -17,6 +18,8 @@ use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Tests\Models\DDC753\DDC753CustomRepository;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemPoolInterface;
@@ -25,6 +28,8 @@ use Psr\Cache\CacheItemPoolInterface;
*/
class ConfigurationTest extends TestCase
{
use VerifyDeprecations;
private Configuration $configuration;
protected function setUp(): void
@@ -34,6 +39,7 @@ class ConfigurationTest extends TestCase
$this->configuration = new Configuration();
}
#[WithoutErrorHandler]
public function testSetGetProxyDir(): void
{
self::assertNull($this->configuration->getProxyDir()); // defaults
@@ -42,6 +48,7 @@ class ConfigurationTest extends TestCase
self::assertSame(__DIR__, $this->configuration->getProxyDir());
}
#[WithoutErrorHandler]
public function testSetGetAutoGenerateProxyClasses(): void
{
self::assertSame(ProxyFactory::AUTOGENERATE_ALWAYS, $this->configuration->getAutoGenerateProxyClasses()); // defaults
@@ -56,6 +63,7 @@ class ConfigurationTest extends TestCase
self::assertSame(ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS, $this->configuration->getAutoGenerateProxyClasses());
}
#[WithoutErrorHandler]
public function testSetGetProxyNamespace(): void
{
self::assertNull($this->configuration->getProxyNamespace()); // defaults
@@ -212,4 +220,13 @@ class ConfigurationTest extends TestCase
$this->configuration->setTypedFieldMapper($defaultTypedFieldMapper);
self::assertSame($defaultTypedFieldMapper, $this->configuration->getTypedFieldMapper());
}
#[RequiresPhp('8.4')]
#[WithoutErrorHandler]
public function testDisablingNativeLazyObjectsIsDeprecated(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12005');
$this->configuration->enableNativeLazyObjects(false);
}
}

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,93 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\Tests\OrmFunctionalTestCase;
use function uniqid;
class PrePersistEventTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
EntityWithUnmappedEntity::class,
EntityWithCascadeAssociation::class,
);
}
public function testCallingPersistInPrePersistHook(): void
{
$entityWithUnmapped = new EntityWithUnmappedEntity();
$entityWithCascade = new EntityWithCascadeAssociation();
$entityWithUnmapped->unmapped = $entityWithCascade;
$entityWithCascade->cascaded = $entityWithUnmapped;
$this->_em->getEventManager()->addEventListener(Events::prePersist, new PrePersistUnmappedPersistListener());
$this->_em->persist($entityWithUnmapped);
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($entityWithCascade));
$this->assertTrue($this->_em->getUnitOfWork()->isScheduledForInsert($entityWithUnmapped));
}
}
class PrePersistUnmappedPersistListener
{
public function prePersist(PrePersistEventArgs $args): void
{
$object = $args->getObject();
if ($object instanceof EntityWithUnmappedEntity) {
$uow = $args->getObjectManager()->getUnitOfWork();
if ($object->unmapped && ! $uow->isInIdentityMap($object->unmapped) && ! $uow->isScheduledForInsert($object->unmapped)) {
$args->getObjectManager()->persist($object->unmapped);
}
}
}
}
#[Entity]
class EntityWithUnmappedEntity
{
#[Id]
#[Column(type: 'string', length: 255)]
#[GeneratedValue(strategy: 'NONE')]
public string $id;
public EntityWithCascadeAssociation|null $unmapped = null;
public function __construct()
{
$this->id = uniqid(self::class, true);
}
}
#[Entity]
class EntityWithCascadeAssociation
{
#[Id]
#[Column(type: 'string', length: 255)]
#[GeneratedValue(strategy: 'NONE')]
public string $id;
#[ManyToOne(targetEntity: EntityWithUnmappedEntity::class, cascade: ['persist'])]
public EntityWithUnmappedEntity|null $cascaded = null;
public function __construct()
{
$this->id = uniqid(self::class, true);
}
}

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

@@ -14,8 +14,6 @@ use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmFunctionalTestCase;
use Doctrine\Tests\Proxies\__CG__\Doctrine\Tests\Models\CMS\CmsUser as CmsUserProxy;
use function assert;
/**
* Test that Doctrine ORM correctly works with proxy instances exactly like with ordinary Entities
*
@@ -79,8 +77,7 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
{
$userId = $this->user->getId();
$uninitializedProxy = $this->_em->getReference(CmsUser::class, $userId);
assert($uninitializedProxy instanceof CmsUserProxy);
self::assertInstanceOf(CmsUserProxy::class, $uninitializedProxy);
$this->assertTrue($this->isUninitializedObject($uninitializedProxy));
$this->_em->persist($uninitializedProxy);
$this->_em->flush();
@@ -112,6 +109,10 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
*/
public function testFindWithProxyName(): void
{
if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) {
self::markTestSkipped('There is no such thing as a proxy class name when native lazy objects are enabled.');
}
$result = $this->_em->find(CmsUserProxy::class, $this->user->getId());
self::assertSame($this->user->getId(), $result->getId());
$this->_em->clear();

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

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Tests\Models\Cache\Country;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use ReflectionProperty;
use function array_diff;
use function array_filter;
use function file_exists;
use function rmdir;
use function scandir;
use function strpos;
use function sys_get_temp_dir;
use const DIRECTORY_SEPARATOR;
/** @phpstan-type SupportedCacheUsage 0|ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE|ClassMetadata::CACHE_USAGE_READ_WRITE */
#[Group('DDC-2183')]
class SecondLevelCacheCountQueriesTest extends SecondLevelCacheFunctionalTestCase
{
/** @var string */
private $tmpDir;
protected function tearDown(): void
{
if ($this->tmpDir !== null && file_exists($this->tmpDir)) {
foreach (array_diff(scandir($this->tmpDir), ['.', '..']) as $f) {
rmdir($this->tmpDir . DIRECTORY_SEPARATOR . $f);
}
rmdir($this->tmpDir);
}
parent::tearDown();
}
/** @param SupportedCacheUsage $cacheUsage */
private function setupCountryModel(int $cacheUsage): void
{
$metadata = $this->_em->getClassMetaData(Country::class);
if ($cacheUsage === 0) {
$metadataCacheReflection = new ReflectionProperty(ClassMetadata::class, 'cache');
$metadataCacheReflection->setAccessible(true);
$metadataCacheReflection->setValue($metadata, null);
return;
}
if ($cacheUsage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
$this->tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::class;
$this->secondLevelCacheFactory->setFileLockRegionDirectory($this->tmpDir);
}
$metadata->enableCache(['usage' => $cacheUsage]);
}
private function loadFixturesCountriesWithoutPostInsertIdentifier(): void
{
$metadata = $this->_em->getClassMetaData(Country::class);
$metadata->setIdGenerator(new AssignedGenerator());
$c1 = new Country('Brazil');
$c1->setId(10);
$c2 = new Country('Germany');
$c2->setId(20);
$this->countries[] = $c1;
$this->countries[] = $c2;
$this->_em->persist($c1);
$this->_em->persist($c2);
$this->_em->flush();
}
/** @param 'INSERT'|'UPDATE'|'DELETE' $type */
private function assertQueryCountByType(string $type, int $expectedCount): void
{
$queries = array_filter($this->getQueryLog()->queries, static function (array $entry) use ($type): bool {
return strpos($entry['sql'], $type) === 0;
});
self::assertCount($expectedCount, $queries);
}
/** @param SupportedCacheUsage $cacheUsage */
#[DataProvider('cacheUsageProvider')]
public function testInsertWithPostInsertIdentifier(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
self::assertQueryCountByType('INSERT', 0);
$this->loadFixturesCountries();
self::assertCount(2, $this->countries);
self::assertQueryCountByType('INSERT', 2);
}
/** @param SupportedCacheUsage $cacheUsage */
#[DataProvider('cacheUsageProvider')]
public function testInsertWithoutPostInsertIdentifier(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
self::assertQueryCountByType('INSERT', 0);
$this->loadFixturesCountriesWithoutPostInsertIdentifier();
self::assertCount(2, $this->countries);
self::assertQueryCountByType('INSERT', 2);
}
/** @param SupportedCacheUsage $cacheUsage */
#[DataProvider('cacheUsageProvider')]
public function testDelete(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
$this->loadFixturesCountries();
$c1 = $this->_em->find(Country::class, $this->countries[0]->getId());
$c2 = $this->_em->find(Country::class, $this->countries[1]->getId());
$this->_em->remove($c1);
$this->_em->remove($c2);
$this->_em->flush();
self::assertQueryCountByType('DELETE', 2);
}
/** @param SupportedCacheUsage $cacheUsage */
#[DataProvider('cacheUsageProvider')]
public function testUpdate(int $cacheUsage): void
{
$this->setupCountryModel($cacheUsage);
$this->loadFixturesCountries();
$c1 = $this->_em->find(Country::class, $this->countries[0]->getId());
$c2 = $this->_em->find(Country::class, $this->countries[1]->getId());
$c1->setName('Czech Republic');
$c2->setName('Hungary');
$this->_em->persist($c1);
$this->_em->persist($c2);
$this->_em->flush();
self::assertQueryCountByType('UPDATE', 2);
}
/** @return list<array{SupportedCacheUsage}> */
public static function cacheUsageProvider(): array
{
return [
[0],
[ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE],
[ClassMetadata::CACHE_USAGE_READ_WRITE],
];
}
}

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

@@ -11,8 +11,6 @@ use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use function sprintf;
/**
* This class makes tests on the correct use of a database schema when entities are stored
*/
@@ -30,32 +28,7 @@ class DDC2825Test extends OrmFunctionalTestCase
}
}
#[DataProvider('getTestedClasses')]
public function testClassSchemaMappingsValidity(string $className, string $expectedSchemaName, string $expectedTableName): void
{
$classMetadata = $this->_em->getClassMetadata($className);
$platform = $this->_em->getConnection()->getDatabasePlatform();
$quotedTableName = $this->_em->getConfiguration()->getQuoteStrategy()->getTableName($classMetadata, $platform);
// Check if table name and schema properties are defined in the class metadata
self::assertEquals($expectedTableName, $classMetadata->table['name']);
self::assertEquals($expectedSchemaName, $classMetadata->table['schema']);
if ($this->_em->getConnection()->getDatabasePlatform()->supportsSchemas()) {
$fullTableName = sprintf('%s.%s', $expectedSchemaName, $expectedTableName);
} else {
$fullTableName = sprintf('%s__%s', $expectedSchemaName, $expectedTableName);
}
self::assertEquals($fullTableName, $quotedTableName);
// Checks sequence name validity
self::assertEquals(
$fullTableName . '_' . $classMetadata->getSingleIdentifierColumnName() . '_seq',
$classMetadata->getSequenceName($platform),
);
}
/** @param class-string $className */
#[DataProvider('getTestedClasses')]
public function testPersistenceOfEntityWithSchemaMapping(string $className): void
{
@@ -68,17 +41,14 @@ class DDC2825Test extends OrmFunctionalTestCase
self::assertCount(1, $this->_em->getRepository($className)->findAll());
}
/**
* Data provider
*
* @return string[][]
*/
/** @return list<array{class-string}> */
public static function getTestedClasses(): array
{
return [
[ExplicitSchemaAndTable::class, 'explicit_schema', 'explicit_table'],
[SchemaAndTableInTableName::class, 'implicit_schema', 'implicit_table'],
[DDC2825ClassWithImplicitlyDefinedSchemaAndQuotedTableName::class, 'myschema', 'order'],
[ExplicitSchemaAndTable::class],
[SchemaAndTableInTableName::class],
[DDC2825ClassWithImplicitlyDefinedSchemaAndQuotedTableName::class],
[File::class],
];
}
}
@@ -93,3 +63,13 @@ class DDC2825ClassWithImplicitlyDefinedSchemaAndQuotedTableName
#[ORM\Column(type: 'integer')]
public $id;
}
#[ORM\Entity]
#[ORM\Table(name: '`file`', schema: 'yourschema')]
class File
{
#[ORM\Id]
#[ORM\Column]
#[ORM\GeneratedValue]
public int $id;
}

View File

@@ -14,8 +14,7 @@ use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function get_class;
use ReflectionClass;
#[Group('GH10808')]
class GH10808Test extends OrmFunctionalTestCase
@@ -51,14 +50,13 @@ class GH10808Test extends OrmFunctionalTestCase
$eagerLoadResult = $query->setHint(UnitOfWork::HINT_DEFEREAGERLOAD, false)->getSingleResult();
self::assertNotEquals(
GH10808AppointmentChild::class,
get_class($deferredLoadResult->child),
'$deferredLoadResult->child should be a proxy',
$reflector = new ReflectionClass(GH10808AppointmentChild::class);
self::assertFalse(
$this->isUninitializedObject($deferredLoadResult->child),
'$deferredLoadResult->child should be a native lazy object',
);
self::assertEquals(
GH10808AppointmentChild::class,
get_class($eagerLoadResult->child),
self::assertFalse(
$this->isUninitializedObject($deferredLoadResult->child),
'$eagerLoadResult->child should not be a proxy',
);
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Schema\Index;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function method_exists;
use function reset;
class GH11982Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH11982ColumnIndex::class,
]);
}
#[Group('GH-11982')]
public function testSchema(): void
{
if ($this->_em->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform) {
self::markTestSkipped('This test does not work on psql.');
}
if (! method_exists(Index::class, 'getIndexedColumns')) {
self::markTestSkipped('This test requires doctrine/dbal >=4.3');
}
$indexes = $this->createSchemaManager()
->introspectTable('GH11982ColumnIndex')
->getIndexes();
self::assertCount(3, $indexes); // primary + 2 custom indexes
self::assertArrayHasKey('class_idx', $indexes);
unset($indexes['primary']);
unset($indexes['class_idx']);
$unnamedIndexColumns = reset($indexes)->getIndexedColumns();
self::assertCount(1, $unnamedIndexColumns);
self::assertEquals(
'indexTrue',
$unnamedIndexColumns[0]->getColumnName()->toString(),
);
}
#[Group('GH-11982')]
public function testSchemaLegacyDbal(): void
{
if ($this->_em->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform) {
self::markTestSkipped('This test does not work on psql.');
}
if (method_exists(Index::class, 'getIndexedColumns')) {
self::markTestSkipped('This test requires doctrine/dbal <4.3');
}
$indexes = $this->createSchemaManager()
->introspectTable('GH11982ColumnIndex')
->getIndexes();
self::assertCount(3, $indexes); // primary + 2 custom indexes
self::assertArrayHasKey('class_idx', $indexes);
unset($indexes['primary']);
unset($indexes['class_idx']);
$unnamedIndexColumns = reset($indexes)->getColumns();
self::assertCount(1, $unnamedIndexColumns);
self::assertEquals('indexTrue', $unnamedIndexColumns[0]);
}
}
#[ORM\Entity]
#[ORM\Index(
name: 'class_idx',
fields: ['classIndex'],
flags: ['fulltext'],
options: ['test'],
)]
class GH11982ColumnIndex
{
#[ORM\Id]
#[ORM\Column]
public string $noIndex;
#[ORM\Column(index: true)]
public string $indexTrue;
#[ORM\Column]
public string $classIndex;
}

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

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Internal\UnitOfWork;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Id\IdentityGenerator;
use Doctrine\ORM\Internal\UnitOfWork\InsertBatch;
use Doctrine\ORM\Mapping\ClassMetadata;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\Stub;
use PHPUnit\Framework\TestCase;
#[CoversClass(InsertBatch::class)]
#[Group('#11977')]
final class InsertBatchTest extends TestCase
{
private EntityManagerInterface&Stub $entityManager;
protected function setUp(): void
{
$this->entityManager = $this->createStub(EntityManagerInterface::class);
$entityAMetadata = new ClassMetadata(EntityA::class);
$entityBMetadata = new ClassMetadata(EntityB::class);
$entityCMetadata = new ClassMetadata(EntityC::class);
$entityAMetadata->idGenerator = new AssignedGenerator();
$entityBMetadata->idGenerator = new AssignedGenerator();
$entityCMetadata->idGenerator = new IdentityGenerator();
$this->entityManager->method('getClassMetadata')
->willReturnMap([
[EntityA::class, $entityAMetadata],
[EntityB::class, $entityBMetadata],
[EntityC::class, $entityCMetadata],
]);
}
public function testWillProduceEmptyBatchOnNoGivenEntities(): void
{
self::assertEmpty(InsertBatch::batchByEntityType($this->entityManager, []));
}
public function testWillBatchSameEntityOperationsInSingleBatch(): void
{
$batches = InsertBatch::batchByEntityType(
$this->entityManager,
[
new EntityA(),
new EntityA(),
new EntityA(),
],
);
self::assertCount(1, $batches);
self::assertSame(EntityA::class, $batches[0]->class->name);
self::assertCount(3, $batches[0]->entities);
}
public function testWillBatchInterleavedEntityOperationsInGroups(): void
{
$batches = InsertBatch::batchByEntityType(
$this->entityManager,
[
new EntityA(),
new EntityA(),
new EntityB(),
new EntityB(),
new EntityA(),
new EntityA(),
],
);
self::assertCount(3, $batches);
self::assertSame(EntityA::class, $batches[0]->class->name);
self::assertCount(2, $batches[0]->entities);
self::assertSame(EntityB::class, $batches[1]->class->name);
self::assertCount(2, $batches[1]->entities);
self::assertSame(EntityA::class, $batches[2]->class->name);
self::assertCount(2, $batches[2]->entities);
}
public function testWillNotBatchOperationsForAGeneratedIdentifierEntity(): void
{
$batches = InsertBatch::batchByEntityType(
$this->entityManager,
[
new EntityC(),
new EntityC(),
new EntityC(),
],
);
self::assertCount(3, $batches);
self::assertSame(EntityC::class, $batches[0]->class->name);
self::assertCount(1, $batches[0]->entities);
self::assertSame(EntityC::class, $batches[1]->class->name);
self::assertCount(1, $batches[1]->entities);
self::assertSame(EntityC::class, $batches[2]->class->name);
self::assertCount(1, $batches[2]->entities);
}
public function testWillIsolateBatchesForEntitiesWithGeneratedIdentifiers(): void
{
$batches = InsertBatch::batchByEntityType(
$this->entityManager,
[
new EntityA(),
new EntityA(),
new EntityC(),
new EntityC(),
new EntityA(),
new EntityA(),
],
);
self::assertCount(4, $batches);
self::assertSame(EntityA::class, $batches[0]->class->name);
self::assertCount(2, $batches[0]->entities);
self::assertSame(EntityC::class, $batches[1]->class->name);
self::assertCount(1, $batches[1]->entities);
self::assertSame(EntityC::class, $batches[2]->class->name);
self::assertCount(1, $batches[2]->entities);
self::assertSame(EntityA::class, $batches[3]->class->name);
self::assertCount(2, $batches[3]->entities);
}
}
class EntityA
{
}
class EntityB
{
}
class EntityC
{
}

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