Compare commits

...

66 Commits
3.4.4 ... 3.5.1

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
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
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
60 changed files with 1659 additions and 283 deletions

View File

@@ -12,38 +12,39 @@
"upcoming": true
},
{
"name": "3.5",
"branchName": "3.5.x",
"slug": "3.5",
"name": "3.6",
"branchName": "3.6.x",
"slug": "3.6",
"upcoming": true
},
{
"name": "3.4",
"branchName": "3.4.x",
"slug": "3.4",
"name": "3.5",
"branchName": "3.5.x",
"slug": "3.5",
"current": true
},
{
"name": "3.4",
"slug": "3.4",
"maintained": false
},
{
"name": "3.3",
"branchName": "3.3.x",
"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
},
@@ -61,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:

View File

@@ -1,7 +1,7 @@
| [4.0.x][4.0] | [3.5.x][3.5] | [3.4.x][3.4] | [2.21.x][2.21] | [2.20.x][2.20] |
| [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.5 image]][3.5] | [![Build status][3.4 image]][3.4] | [![Build status][2.21 image]][2.21] | [![Build status][2.20 image]][2.20] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.5 coverage image]][3.5 coverage] | [![Coverage Status][3.4 coverage image]][3.4 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
| [![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.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
[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
[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,3 +1,49 @@
# 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
@@ -23,6 +69,8 @@ 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`

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

@@ -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;

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

@@ -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

@@ -1333,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
-
@@ -1386,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
@@ -1434,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
@@ -2022,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
@@ -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
@@ -3121,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
@@ -3157,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
@@ -3289,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
-

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
-

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;
@@ -63,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;
}
@@ -71,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;
}
@@ -81,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;
}
@@ -91,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;
}
@@ -99,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;
}
@@ -107,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;
}
@@ -602,6 +657,14 @@ class Configuration extends \Doctrine\DBAL\Configuration
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.');
}
@@ -610,7 +673,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
}
/**
* To be deprecated in 3.1.0
* @deprecated lazy ghost objects are always enabled
*
* @return true
*/
@@ -619,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) {
@@ -630,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) {
@@ -642,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

@@ -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

@@ -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

@@ -400,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{
@@ -544,6 +540,8 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
/**
* The ReflectionProperty instances of the mapped class.
*
* @deprecated Use $propertyAccessors instead.
*
* @var LegacyReflectionFields|array<string, ReflectionProperty>
*/
public LegacyReflectionFields|array $reflFields = [];
@@ -573,6 +571,8 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
/**
* Gets the ReflectionProperties of the mapped class.
*
* @deprecated Use getPropertyAccessors() instead.
*
* @return LegacyReflectionFields|ReflectionProperty[] An array of ReflectionProperty instances.
* @phpstan-return LegacyReflectionFields|array<string, ReflectionProperty>
*/
@@ -593,6 +593,8 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
/**
* Gets a ReflectionProperty for a specific field of the mapped class.
*
* @deprecated Use getPropertyAccessor() instead.
*/
public function getReflectionProperty(string $name): ReflectionProperty|null
{
@@ -604,7 +606,11 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
return $this->propertyAccessors[$name] ?? null;
}
/** @throws BadMethodCallException If the class has a composite identifier. */
/**
* @deprecated Use getPropertyAccessor() instead.
*
* @throws BadMethodCallException If the class has a composite identifier.
*/
public function getSingleIdReflectionProperty(): ReflectionProperty|null
{
if ($this->isIdentifierComposite) {
@@ -818,7 +824,8 @@ 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();
@@ -1056,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

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

@@ -710,6 +710,7 @@ class AttributeDriver implements MappingDriver
* length: int,
* unique: bool,
* nullable: bool,
* index: bool,
* precision: int,
* enumType?: class-string,
* options?: mixed[],
@@ -726,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
@@ -93,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,

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

@@ -693,11 +693,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;
}
}
@@ -1964,6 +1964,7 @@ class BasicEntityPersister implements EntityPersister
ParameterType::STRING => ArrayParameterType::STRING,
ParameterType::INTEGER => ArrayParameterType::INTEGER,
ParameterType::ASCII => ArrayParameterType::ASCII,
ParameterType::BINARY => ArrayParameterType::BINARY,
};
}

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;
@@ -30,6 +31,7 @@ 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;
@@ -150,12 +152,29 @@ EOPHP;
string|null $proxyNs = null,
bool|int $autoGenerate = self::AUTOGENERATE_NEVER,
) {
if (! $proxyDir && ! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
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 && ! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
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)) {

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

@@ -505,6 +505,11 @@ class SchemaTool
if ($isUnique) {
$table->addUniqueIndex([$columnName]);
}
$isIndex = $mapping->index ?? false;
if ($isIndex) {
$table->addIndex([$columnName]);
}
}
/**

View File

@@ -153,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 ' .
@@ -174,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 ' .

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;
@@ -1037,30 +1038,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];
}
}
}

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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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
{
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Mapping;
use ArrayObject;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Events;
@@ -51,6 +52,7 @@ use Doctrine\Tests\ORM\Mapping\TypedFieldMapper\CustomIntAsStringTypedFieldMappe
use Doctrine\Tests\OrmTestCase;
use DoctrineGlobalArticle;
use LogicException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group as TestGroup;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use ReflectionClass;
@@ -60,6 +62,7 @@ use function array_keys;
use function assert;
use function class_exists;
use function count;
use function defined;
use function serialize;
use function str_contains;
use function str_replace;
@@ -199,6 +202,12 @@ class ClassMetadataTest extends OrmTestCase
// float
$cm->mapField(['fieldName' => 'float']);
self::assertEquals('float', $cm->getTypeOfField('float'));
// number, requires DBAL 4.3+
if (defined(Types::class . '::NUMBER')) {
$cm->mapField(['fieldName' => 'bodyHeight']);
self::assertEquals('number', $cm->getTypeOfField('bodyHeight'));
}
}
#[TestGroup('GH10313')]
@@ -963,6 +972,47 @@ class ClassMetadataTest extends OrmTestCase
);
}
#[DataProvider('fullTableNameProvider')]
public function testGetSequenceName(
string $expectedSequenceName,
string $fullTableName,
): void {
$cm = new ClassMetadata(self::class);
$cm->setIdentifier(['id']);
$cm->setPrimaryTable(['name' => $fullTableName]);
$platform = $this->createStub(AbstractPlatform::class);
self::assertSame(
$expectedSequenceName,
$cm->getSequenceName($platform),
);
}
/** @return iterable<string, array{string, string}> */
public static function fullTableNameProvider(): iterable
{
yield 'quoted table name with schema' => [
'custom.reserved_id_seq',
'custom.`reserved`',
];
yield 'unquoted table name with schema' => [
'custom.non_reserved_id_seq',
'custom.non_reserved',
];
yield 'quoted table name without schema' => [
'reserved_id_seq',
'`reserved`',
];
yield 'unquoted table name without schema' => [
'non_reserved_id_seq',
'non_reserved',
];
}
#[TestGroup('DDC-2700')]
public function testIsIdentifierMappedSuperClass(): void
{

View File

@@ -6,13 +6,16 @@ namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Name\UnquotedIdentifierFolding;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\DefaultQuoteStrategy;
use Doctrine\Tests\Models\NonPublicSchemaJoins\User as NonPublicSchemaUser;
use Doctrine\Tests\OrmTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use function assert;
use function enum_exists;
use function sprintf;
/**
* Doctrine\Tests\ORM\Mapping\DefaultQuoteStrategyTest
@@ -34,4 +37,48 @@ class DefaultQuoteStrategyTest extends OrmTestCase
$strategy->getJoinTableName($metadata->associationMappings['readers'], $metadata, $platform),
);
}
#[DataProvider('fullTableNameProvider')]
public function testGetTableName(string $expectedFullTableName, string $tableName): void
{
$classMetadata = new ClassMetadata(self::class);
$classMetadata->setPrimaryTable(['name' => $tableName]);
$platform = $this->createStub(AbstractPlatform::class);
$platform->method('quoteSingleIdentifier')
->willReturnCallback(
static fn (string $identifier): string => sprintf('✌️%s✌', $identifier),
);
$quotedTableName = (new DefaultQuoteStrategy())->getTableName(
$classMetadata,
$platform,
);
self::assertSame($expectedFullTableName, $quotedTableName);
}
/** @return iterable<string, array{string, string}> */
public static function fullTableNameProvider(): iterable
{
yield 'quoted table name with schema' => [
'✌custom✌.✌reserved✌',
'custom.`reserved`',
];
yield 'unquoted table name with schema' => [
'custom.non_reserved',
'custom.non_reserved',
];
yield 'quoted table name without schema' => [
'✌reserved✌',
'`reserved`',
];
yield 'unquoted table name without schema' => [
'non_reserved',
'non_reserved',
];
}
}

View File

@@ -227,11 +227,12 @@ abstract class MappingDriverTestCase extends OrmTestCase
#[Depends('testEntityTableNameAndInheritance')]
public function testFieldMappings(ClassMetadata $class): ClassMetadata
{
self::assertEquals(4, count($class->fieldMappings));
self::assertEquals(5, count($class->fieldMappings));
self::assertTrue(isset($class->fieldMappings['id']));
self::assertTrue(isset($class->fieldMappings['name']));
self::assertTrue(isset($class->fieldMappings['email']));
self::assertTrue(isset($class->fieldMappings['version']));
self::assertTrue(isset($class->fieldMappings['indexed']));
return $class;
}
@@ -262,6 +263,7 @@ abstract class MappingDriverTestCase extends OrmTestCase
self::assertEquals(50, $class->fieldMappings['name']->length);
self::assertTrue($class->fieldMappings['name']->nullable);
self::assertTrue($class->fieldMappings['name']->unique);
self::assertTrue($class->fieldMappings['indexed']->index);
return $class;
}
@@ -1006,6 +1008,10 @@ class User
#[ORM\Version]
public $version;
/** @var string */
#[ORM\Column(index: true)]
public $indexed;
#[ORM\PrePersist]
public function doStuffOnPrePersist(): void
{
@@ -1065,6 +1071,12 @@ class User
$mapping = ['fieldName' => 'version', 'type' => 'integer'];
$metadata->setVersionMapping($mapping);
$metadata->mapField($mapping);
$metadata->mapField([
'fieldName' => 'indexed',
'type' => 'string',
'columnName' => 'indexed',
'index' => true,
]);
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO);
$metadata->mapOneToOne(
[

View File

@@ -11,10 +11,13 @@ use Doctrine\ORM\Mapping\TypedFieldMapper;
use Doctrine\Tests\Models\TypedProperties\UserTyped;
use Doctrine\Tests\ORM\Mapping\TypedFieldMapper\CustomIntAsStringTypedFieldMapper;
use Doctrine\Tests\OrmTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use ReflectionClass;
use function defined;
#[Group('GH10313')]
class TypedFieldMapperTest extends OrmTestCase
{
@@ -36,7 +39,7 @@ class TypedFieldMapperTest extends OrmTestCase
/**
* Data Provider for NamingStrategy#classToTableName
*
* @return array<
* @return Generator<
* array{
* TypedFieldMapper,
* ReflectionClass,
@@ -44,28 +47,30 @@ class TypedFieldMapperTest extends OrmTestCase
* array{fieldName: string, enumType?: string, type?: mixed}
* }>
*/
public static function dataFieldToMappedField(): array
public static function dataFieldToMappedField(): Generator
{
$reflectionClass = new ReflectionClass(UserTyped::class);
return [
// DefaultTypedFieldMapper
[self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'id'], ['fieldName' => 'id', 'type' => Types::INTEGER]],
[self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'username'], ['fieldName' => 'username', 'type' => Types::STRING]],
[self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'dateInterval'], ['fieldName' => 'dateInterval', 'type' => Types::DATEINTERVAL]],
[self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'dateTime'], ['fieldName' => 'dateTime', 'type' => Types::DATETIME_MUTABLE]],
[self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'dateTimeImmutable'], ['fieldName' => 'dateTimeImmutable', 'type' => Types::DATETIME_IMMUTABLE]],
[self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'array'], ['fieldName' => 'array', 'type' => Types::JSON]],
[self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'boolean'], ['fieldName' => 'boolean', 'type' => Types::BOOLEAN]],
[self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'float'], ['fieldName' => 'float', 'type' => Types::FLOAT]],
// DefaultTypedFieldMapper
yield [self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'id'], ['fieldName' => 'id', 'type' => Types::INTEGER]];
yield [self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'username'], ['fieldName' => 'username', 'type' => Types::STRING]];
yield [self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'dateInterval'], ['fieldName' => 'dateInterval', 'type' => Types::DATEINTERVAL]];
yield [self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'dateTime'], ['fieldName' => 'dateTime', 'type' => Types::DATETIME_MUTABLE]];
yield [self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'dateTimeImmutable'], ['fieldName' => 'dateTimeImmutable', 'type' => Types::DATETIME_IMMUTABLE]];
yield [self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'array'], ['fieldName' => 'array', 'type' => Types::JSON]];
yield [self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'boolean'], ['fieldName' => 'boolean', 'type' => Types::BOOLEAN]];
yield [self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'float'], ['fieldName' => 'float', 'type' => Types::FLOAT]];
// CustomIntAsStringTypedFieldMapper
[self::customTypedFieldMapper(), $reflectionClass, ['fieldName' => 'id'], ['fieldName' => 'id', 'type' => Types::STRING]],
if (defined(Types::class . '::NUMBER')) {
yield [self::defaultTypedFieldMapper(), $reflectionClass, ['fieldName' => 'bodyHeight'], ['fieldName' => 'bodyHeight', 'type' => Types::NUMBER]];
}
// ChainTypedFieldMapper
[self::chainTypedFieldMapper(), $reflectionClass, ['fieldName' => 'id'], ['fieldName' => 'id', 'type' => Types::STRING]],
[self::chainTypedFieldMapper(), $reflectionClass, ['fieldName' => 'username'], ['fieldName' => 'username', 'type' => Types::STRING]],
];
// CustomIntAsStringTypedFieldMapper
yield [self::customTypedFieldMapper(), $reflectionClass, ['fieldName' => 'id'], ['fieldName' => 'id', 'type' => Types::STRING]];
// ChainTypedFieldMapper
yield [self::chainTypedFieldMapper(), $reflectionClass, ['fieldName' => 'id'], ['fieldName' => 'id', 'type' => Types::STRING]];
yield [self::chainTypedFieldMapper(), $reflectionClass, ['fieldName' => 'username'], ['fieldName' => 'username', 'type' => Types::STRING]];
}
/**

View File

@@ -183,7 +183,7 @@ class XmlMappingDriverTest extends MappingDriverTestCase
[
User::class,
'cms_users',
['name', 'email', 'version', 'id'],
['name', 'email', 'version', 'indexed', 'id'],
['address', 'phonenumbers', 'groups'],
],
[

View File

@@ -56,6 +56,8 @@
<field name="version" type="integer" version="true" />
<field name="indexed" type="string" index="true" />
<one-to-one field="address" target-entity="Address" inversed-by="user">
<cascade><cascade-remove /></cascade>
<join-column name="address_id" referenced-column-name="id" on-delete="CASCADE" />

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\Mapping as MappingNamespace;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
@@ -12,6 +13,7 @@ use Doctrine\ORM\ORMSetup;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\Attributes\RequiresSetting;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
@@ -20,10 +22,19 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter;
use function sys_get_temp_dir;
use const PHP_VERSION_ID;
class ORMSetupTest extends TestCase
{
use VerifyDeprecations;
#[WithoutErrorHandler]
public function testAttributeConfiguration(): void
{
if (PHP_VERSION_ID >= 80400) {
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12005');
}
$config = ORMSetup::createAttributeMetadataConfiguration([], true);
self::assertInstanceOf(Configuration::class, $config);
@@ -32,8 +43,21 @@ class ORMSetupTest extends TestCase
self::assertInstanceOf(AttributeDriver::class, $config->getMetadataDriverImpl());
}
public function testAttributeConfig(): void
{
$config = ORMSetup::createAttributeMetadataConfig([], true);
self::assertInstanceOf(Configuration::class, $config);
self::assertInstanceOf(AttributeDriver::class, $config->getMetadataDriverImpl());
}
#[WithoutErrorHandler]
public function testXMLConfiguration(): void
{
if (PHP_VERSION_ID >= 80400) {
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12005');
}
$config = ORMSetup::createXMLMetadataConfiguration([], true);
self::assertInstanceOf(Configuration::class, $config);
@@ -44,14 +68,18 @@ class ORMSetupTest extends TestCase
{
$this->expectNotToPerformAssertions();
ORMSetup::createXMLMetadataConfiguration(paths: [], isXsdValidationEnabled: false);
ORMSetup::createXMLMetadataConfig(paths: [], isXsdValidationEnabled: false);
}
#[RequiresPhpExtension('apcu')]
#[RequiresSetting('apc.enable_cli', '1')]
#[RequiresSetting('apc.enabled', '1')]
public function testCacheNamespaceShouldBeGeneratedForApcu(): void
public function testCacheNamespaceShouldBeGeneratedForApcuWhenUsingLegacyConstructor(): void
{
if (PHP_VERSION_ID >= 80400) {
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12005');
}
$config = ORMSetup::createConfiguration(false, '/foo');
$cache = $config->getMetadataCache();
@@ -61,7 +89,22 @@ class ORMSetupTest extends TestCase
self::assertSame('dc2_1effb2475fcfba4f9e8b8a1dbc8f3caf:', $namespaceProperty->getValue($cache));
}
#[RequiresPhpExtension('apcu')]
#[RequiresSetting('apc.enable_cli', '1')]
#[RequiresSetting('apc.enabled', '1')]
public function testCacheNamespaceShouldBeGeneratedForApcu(): void
{
$config = ORMSetup::createConfig(false, '/foo');
$cache = $config->getMetadataCache();
$namespaceProperty = new ReflectionProperty(AbstractAdapter::class, 'namespace');
self::assertInstanceOf(ApcuAdapter::class, $cache);
self::assertSame('dc2_1effb2475fcfba4f9e8b8a1dbc8f3caf:', $namespaceProperty->getValue($cache));
}
#[Group('DDC-1350')]
#[WithoutErrorHandler]
public function testConfigureProxyDir(): void
{
$config = ORMSetup::createAttributeMetadataConfiguration([], true, '/foo');
@@ -72,7 +115,7 @@ class ORMSetupTest extends TestCase
public function testConfigureCache(): void
{
$cache = new ArrayAdapter();
$config = ORMSetup::createAttributeMetadataConfiguration([], true, null, $cache);
$config = ORMSetup::createAttributeMetadataConfig([], true, null, $cache);
self::assertSame($cache, $config->getResultCache());
self::assertSame($cache, $config->getQueryCache());
@@ -83,7 +126,7 @@ class ORMSetupTest extends TestCase
public function testConfigureCacheCustomInstance(): void
{
$cache = new ArrayAdapter();
$config = ORMSetup::createConfiguration(true, null, $cache);
$config = ORMSetup::createConfig(true, null, $cache);
self::assertSame($cache, $config->getResultCache());
self::assertSame($cache, $config->getQueryCache());

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Persisters;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Types\Type as DbalType;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMSetup;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\ORM\Tools\SchemaValidator;
use Doctrine\Tests\Mocks\EntityManagerMock;
use Doctrine\Tests\Models\BinaryPrimaryKey\BinaryIdType;
use Doctrine\Tests\Models\BinaryPrimaryKey\Category;
use Doctrine\Tests\OrmTestCase;
final class BinaryIdPersisterTest extends OrmTestCase
{
private EntityManager|null $entityManager = null;
public function testOneToManyWithEagerFetchMode(): void
{
$entityManager = $this->createEntityManager();
$this->createDummyBlogData($entityManager, 3);
$categories = $entityManager->createQueryBuilder()
->select('category')
->from(Category::class, 'category')
->getQuery()
->setFetchMode(Category::class, 'children', ClassMetadata::FETCH_EAGER)
->getResult();
self::assertCount(3, $categories);
}
private function createDummyBlogData(
EntityManager $entityManager,
int $categoryCount = 1,
int $categoryParentsCount = 0,
): void {
for ($h = 0; $h < $categoryCount; $h++) {
$categoryParent = null;
for ($i = 0; $i < $categoryParentsCount; $i++) {
$categoryParent = new Category('CategoryParent#' . $i, $categoryParent);
$entityManager->persist($categoryParent);
}
$category = new Category('Category#' . $h, $categoryParent);
$entityManager->persist($category);
}
$entityManager->flush();
$entityManager->clear();
}
private function createEntityManager(): EntityManager
{
if ($this->entityManager !== null) {
return $this->entityManager;
}
$config = ORMSetup::createAttributeMetadataConfiguration([__DIR__ . '/../../Models/BinaryPrimaryKey'], isDevMode: true);
if (! DbalType::hasType(BinaryIdType::NAME)) {
DbalType::addType(BinaryIdType::NAME, BinaryIdType::class);
}
$connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config);
$entityManager = new EntityManagerMock($connection, $config);
$schemaTool = new SchemaTool($entityManager);
$schemaTool->createSchema($entityManager->getMetadataFactory()->getAllMetadata());
$schemaValidator = new SchemaValidator($entityManager);
$schemaValidator->validateMapping();
$this->entityManager = $entityManager;
return $entityManager;
}
}

View File

@@ -7,6 +7,7 @@ namespace Doctrine\Tests\ORM\Proxy;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
@@ -21,6 +22,7 @@ use Doctrine\Tests\Models\ECommerce\ECommerceFeature;
use Doctrine\Tests\OrmTestCase;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use ReflectionClass;
use ReflectionProperty;
use stdClass;
@@ -34,10 +36,10 @@ use function sys_get_temp_dir;
*/
class ProxyFactoryTest extends OrmTestCase
{
use VerifyDeprecations;
private UnitOfWorkMock $uowMock;
private EntityManagerMock $emMock;
private ProxyFactory $proxyFactory;
protected function setUp(): void
@@ -243,6 +245,37 @@ class ProxyFactoryTest extends OrmTestCase
self::assertTrue($reflection->isUninitializedLazyObject($proxy));
}
#[RequiresPhp('8.4')]
#[WithoutErrorHandler]
public function testProxyFactoryTriggersDeprecationWhenNativeLazyObjectsAreDisabled(): void
{
$this->emMock->getConfiguration()->enableNativeLazyObjects(false);
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12005');
$this->proxyFactory = new ProxyFactory(
$this->emMock,
sys_get_temp_dir(),
'Proxies',
ProxyFactory::AUTOGENERATE_ALWAYS,
);
}
#[RequiresPhp('< 8.4')]
public function testProxyFactoryDoesNotTriggerDeprecationWhenNativeLazyObjectsAreDisabled(): void
{
$this->emMock->getConfiguration()->enableNativeLazyObjects(false);
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12005');
$this->proxyFactory = new ProxyFactory(
$this->emMock,
sys_get_temp_dir(),
'Proxies',
ProxyFactory::AUTOGENERATE_ALWAYS,
);
}
}
abstract class AbstractClass

View File

@@ -11,7 +11,9 @@ use Doctrine\Tests\Models\Cache\AttractionInfo;
use Doctrine\Tests\OrmFunctionalTestCase;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandCompletionTester;
use Symfony\Component\Console\Tester\CommandTester;
/**
@@ -77,4 +79,37 @@ class MappingDescribeCommandTest extends OrmFunctionalTestCase
],
);
}
/**
* @param string[] $input
* @param string[] $expectedSuggestions
*/
#[DataProvider('provideCompletionSuggestions')]
public function testComplete(array $input, array $expectedSuggestions): void
{
$this->useModelSet('cache');
parent::setUp();
$completionTester = new CommandCompletionTester(new MappingDescribeCommand(new SingleManagerProvider($this->_em)));
$suggestions = $completionTester->complete($input);
foreach ($expectedSuggestions as $expected) {
self::assertContains($expected, $suggestions);
}
}
/** @return iterable<string, array{string[], string[]}> */
public static function provideCompletionSuggestions(): iterable
{
yield 'entityName' => [
[''],
[
'Doctrine\\\\Tests\\\\Models\\\\Cache\\\\Restaurant',
'Doctrine\\\\Tests\\\\Models\\\\Cache\\\\Beach',
'Doctrine\\\\Tests\\\\Models\\\\Cache\\\\Bar',
],
];
}
}

View File

@@ -135,7 +135,7 @@ class SchemaValidatorTest extends OrmTestCase
[
'The field Doctrine\Tests\ORM\Tools\DDC3274One#two is on the inverse side of a bi-directional ' .
'relationship, but the specified mappedBy association on the target-entity ' .
"Doctrine\Tests\ORM\Tools\DDC3274Two#one does not contain the required 'inversedBy=\"two\"' attribute.",
"Doctrine\Tests\ORM\Tools\DDC3274Two#one does not contain the required 'inversedBy: \"two\"' attribute.",
],
$ce,
);
@@ -151,7 +151,7 @@ class SchemaValidatorTest extends OrmTestCase
[
'The field Doctrine\Tests\ORM\Tools\Issue9536Owner#one is on the owning side of a bi-directional ' .
'relationship, but the specified inversedBy association on the target-entity ' .
"Doctrine\Tests\ORM\Tools\Issue9536Target#two does not contain the required 'mappedBy=\"one\"' " .
"Doctrine\Tests\ORM\Tools\Issue9536Target#two does not contain the required 'mappedBy: \"one\"' " .
'attribute.',
],
$ce,

View File

@@ -33,6 +33,7 @@ use Doctrine\Tests\Models\CMS\CmsPhonenumber;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\Models\Forum\ForumAvatar;
use Doctrine\Tests\Models\Forum\ForumUser;
use Doctrine\Tests\Models\MixedToOneIdentity\Country;
use Doctrine\Tests\OrmTestCase;
use Exception as BaseException;
use PHPUnit\Framework\Attributes\DataProvider;
@@ -143,6 +144,25 @@ class UnitOfWorkTest extends OrmTestCase
self::assertIsNumeric($user->id);
}
#[Group('#11977')]
public function testMultipleInsertsAreBatchedInThePersister(): void
{
$userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(Country::class));
$this->_unitOfWork->setEntityPersister(Country::class, $userPersister);
$country1 = new Country();
$country1->country = 'Italy';
$country2 = new Country();
$country2->country = 'Germany';
$this->_unitOfWork->persist($country1);
$this->_unitOfWork->persist($country2);
$this->_unitOfWork->commit();
self::assertCount(2, $userPersister->getInserts());
self::assertSame(1, $userPersister->countOfExecuteInsertCalls());
}
/**
* Tests a scenario where a save() operation is cascaded from a ForumUser
* to its associated ForumAvatar, both entities using IDENTITY id generation.

View File

@@ -6,6 +6,7 @@ namespace Doctrine\Tests\Proxy;
use Doctrine\ORM\Proxy\Autoloader;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use PHPUnit\Framework\TestCase;
use function class_exists;
@@ -31,6 +32,7 @@ class AutoloaderTest extends TestCase
/** @param class-string $className */
#[DataProvider('dataResolveFile')]
#[WithoutErrorHandler]
public function testResolveFile(
string $proxyDir,
string $proxyNamespace,
@@ -41,6 +43,7 @@ class AutoloaderTest extends TestCase
self::assertEquals($expectedProxyFile, $actualProxyFile);
}
#[WithoutErrorHandler]
public function testAutoload(): void
{
if (file_exists(sys_get_temp_dir() . '/AutoloaderTestClass.php')) {

View File

@@ -100,14 +100,14 @@ class TestUtil
$enableNativeLazyObjects = true;
}
$configuration->setProxyDir(__DIR__ . '/Proxies');
$configuration->setProxyNamespace('Doctrine\Tests\Proxies');
if (PHP_VERSION_ID >= 80400 && $enableNativeLazyObjects) {
$configuration->enableNativeLazyObjects(true);
return;
}
$configuration->setProxyDir(__DIR__ . '/Proxies');
$configuration->setProxyNamespace('Doctrine\Tests\Proxies');
}
private static function initializeDatabase(): void