Compare commits

...

75 Commits

Author SHA1 Message Date
Grégoire Paris a64f315dfe Merge pull request #10642 from yobrx/patch-1 2023-04-20 11:46:32 +02:00
Yoann B 6ca319a6f4 fix array association on partial index 2023-04-20 10:59:39 +02:00
Grégoire Paris fceb279947 Merge pull request #10630 from monadial/fix/fqcn-type-in-xml-mapping
Fixed xsd schema for support FQCN type
2023-04-16 10:26:37 +02:00
Alexander M. Turek 2977933119 Run tests on SQLite with foreign keys enabled (#10632) 2023-04-15 10:54:31 +02:00
Tomas Mihalicka 5ac6fadf29 Fixed xsd schema for support FQCN type
After update to orm 2.14.2 invalid xsd schema error is occured, when in field,id or attribute-override have type is FQCN
2023-04-14 18:16:35 +02:00
Grégoire Paris e59ed88251 Merge pull request #10620 from ecourtial/fix-doc-typo
fix typo in HydrationCompleteHandler doc
2023-04-12 21:21:42 +02:00
Eric COURTIAL a16aeaeac8 fix typo in HydrationCompleteHandler doc 2023-04-12 21:02:36 +02:00
Mathieu fca1ef78a7 Handle null comparisons in ManyToManyPersister (#10587)
* Add test case for https://github.com/doctrine/orm/issues/7717

* Do not hide null equality checks in `SqlValueVisitor::walkComparison`

* Annotate `GH7717Parent::$children` type
2023-04-12 17:31:38 +02:00
Grégoire Paris e5fb1a4a8f Merge pull request #10604 from greg0ire/psalm-5.9.0 2023-03-30 17:18:54 +02:00
Grégoire Paris 8cb7a5e212 Upgrade to Psalm 5.9.0
It looks like some issues are now represented by different classes.
2023-03-30 17:05:25 +02:00
Grégoire Paris da9b9de590 Merge pull request #10589 from e2palmes/patch-1 2023-03-22 12:12:27 +01:00
Emmanuel DE PALMÈS 9ec697db7d Added missing ';'
missing ';' on Obtaining the EntityManager section, bootstrap code line 32
2023-03-22 11:52:36 +01:00
Mikael Peigney a50a611bee docs: Remove incorrect @SequenceGenerator info (#10583)
The default allocationSize for @SequenceGenerator has actually been set to 1 ever since 434325e (back in 2010!)
This was done to remove confusion, but the docs were never updated to reflect the change
2023-03-16 10:32:42 +01:00
David Maicher abb30093ed add $isXsdValidationEnabled to SimplifiedXmlDriver constructor 2023-03-14 09:38:23 +01:00
Alexander M. Turek df559d8ac0 PHPStan 1.10.6, Psalm 5.8.0 (#10575) 2023-03-14 07:03:59 +01:00
Cyril Beslay a6de4b9663 docs: Update Logger removal in Batch Processing documentation (#10572)
Current documented way is using Doctrine DBAL2.
Since DBAL3, logging is done with Middlewares, so I updated the recommendation in documentation.
2023-03-12 00:09:30 +01:00
Grégoire Paris 6db296cd5c Merge pull request #10539 from mpdude/performance-to-one-inheritance
More precisely document the performance impact of to-one associations towards inheritance hierarchies
2023-03-08 23:00:08 +01:00
Matthias Pigulla 7c2adde6f2 More precisely document the performance impact of to-one associations towards inheritance hierarchies
This puts the remarks that apply to both JTI/STI in a common section at the beginning, and tries to explain a bit more in detail why it is that way.

It was sparked off by the discussion in #10538.
2023-03-08 17:27:44 +00:00
Grégoire Paris 69543ba1bf Skip test instead of commenting it out (#10563)
Skipping makes the test more discoverable.
2023-03-06 09:27:35 +01:00
Alexander M. Turek a33885575f Skip test instead of commenting it out (#10560) 2023-03-05 22:24:44 +01:00
Gabriel Ostrolucký e28dee0742 Add missing return statements to Command:configure methods 2023-03-05 21:31:46 +01:00
Alexander M. Turek a28e2d8277 Ignore the cache dir of PHPUnit 10 (#10546) 2023-02-28 13:35:35 +01:00
Alexander M. Turek 4759a1bf75 Make data providers static (#10544) 2023-02-28 08:29:28 +01:00
Alexander M. Turek e0ad7ac506 Bump dev tools (#10541)
* phpstan/phpstan (1.9.14 => 1.10.3)
* squizlabs/php_codesniffer (3.7.1 => 3.7.2)
* vimeo/psalm (5.6.0 => 5.7.7)
2023-02-28 08:28:45 +01:00
Christophe Coevoet 9485d4d835 Mark SqlWalker methods as not deprecated (#10540)
phpstan treats implementations of deprecated methods of an interface as being deprecated themselves by default.
However, SqlWalker does not intend to deprecate all those methods that are deprecated in TreeWalker, as they are
moved down. Marking them as not deprecated will avoid reporting usages of deprecated APIs when implementing
custom DQL functions for instance.
2023-02-26 15:21:47 +01:00
Matthieu Lempereur 9a6e1b3505 docs: consistency order for docblock in association mapping (#10534) 2023-02-22 14:00:30 +01:00
Grégoire Paris c286742814 Merge pull request #10529 from joshpme/patch-1
Correct use of PHP attribute
2023-02-20 08:00:20 +01:00
Josh P 052887765b Correct use of PHP attribute
Incorrect syntax used in php 8 attribute. This should be key: value, rather than key=value
2023-02-20 15:37:30 +11:00
Al Zee 5464e21022 fix typo in faq.rst (#10526) 2023-02-17 08:25:10 +01:00
Grégoire Paris c26c55926f Merge pull request #8797 from mpdude/query_count_hint 2023-02-15 17:15:45 +01:00
Simon Podlipsky 5369e4f425 fix: use executeStatement in SchemaTool (#10516) 2023-02-14 19:14:28 +01:00
Matthias Pigulla 29bc6cc955 Write a test in a more specific way
... so we can be sure that in fact the second result has a different size.

Co-authored-by: Luís Cobucci <lcobucci@gmail.com>
2023-02-14 08:04:56 +00:00
Matthias Pigulla 31ff969628 Put up a warning sign that mapping may not be inherited from transient classes (#10392)
This _seems_ to work, but...

To my understanding, that is only because `ReflectionClass::getProperties` will report `protected` properties also for subclasses, including DocBlocks etc. The mapping drivers are unable to tell where a field comes from, so they pick up the mapping and treat it as originating from the inheriting class.

If that is indeed outside of what the ORM was designed for (sombody please confirm?), then we should explicitly discourage it.

Yes, I have seen examples of this in the wild.
2023-02-09 00:19:38 +01:00
Alexander M. Turek 01f139d76c Run tests with ext-pgsql (#10480) 2023-02-08 09:33:05 +01:00
Matthias Pigulla 660197ea71 Avoid unnecessary information in query hints to improve query cache hit ratio
I've noticed that over time my query caches fill up with redundant queries, i. e. different cache entries for the DQL -> SQL translation that are exactly the same. For me, it's an issue because the cache entries fill up precious OPcache memory.

Further investigation revealed that the queries themselves do not differ, but only the query hints – that are part of the computed cache key – do.

In particular, only the value for the `WhereInWalker::HINT_PAGINATOR_ID_COUNT` query hint are different. Since `WhereInWalker` only needs to know _if_ there are matching IDs but not _how many_, we could avoid such cache misses by using just a boolean value as cache hint.
2023-02-08 08:19:25 +01:00
Alexander M. Turek ab06e07b53 Baseline Psalm errors for DBAL 3.6 (#10507) 2023-02-08 07:53:21 +01:00
Grégoire Paris 022b945ed5 Merge pull request #10444 from mpdude/paginator-dql-cacheable
Make Paginator-internal query cacheable in the query cache
2023-02-08 07:53:12 +01:00
Sebastian Busch 7203d05539 Clarify difference between transactional() methods of Connection and EntityManager (#10133)
One could interpret the old description as if `Connection#transactional()` would not rollback the transaction. Also, the fact that the `EntityManager` gets closed in case of an exception was not mentioned.
2023-02-07 23:38:04 +01:00
Alexander M. Turek 0bd5fbf215 Remove calls to assertObjectHasAttribute() (#10502) 2023-02-07 00:13:13 +01:00
Alexander M. Turek 6a713dd39e Remove calls to withConsecutive() (#10501) 2023-02-06 23:23:05 +01:00
Grégoire Paris 5f169d9325 Merge pull request #10420 from mpdude/fix-9095
Fix #9095 by re-applying #9096
2023-02-06 08:23:15 +01:00
Grégoire Paris cf4680d0e6 Merge pull request #10498 from greg0ire/fix-invalid-test
Use recognized array key
2023-02-06 08:22:26 +01:00
Matthias Pigulla cc5775c3f4 Make class final (as suggested in GH review) 2023-02-05 21:38:03 +00:00
Grégoire Paris c5cf6a046b Use recognized array key
"joinColumn" has no meaning for the static PHP driver. That's because it
performs no translation, unlike other drivers: we're using the
ClassMetadata API directly.

Before this change, changing the value of the "name" key did not break
the test suite. After this change, it does.
2023-02-05 14:35:06 +01:00
Matthias Pigulla 77df5db3b9 Make Paginator-internal query cacheable in the query cache
Make the `Paginator`-internal query (`... WHERE ... IN (id, id2,
id3...)`) cacheable in the query cache again.

When the Paginator creates the internal subquery that does the actual
result limiting, it has to take DBAL type conversions for the identifier
column of the paginated root entity into account (#7820, fixed in
 #7821).

In order to perform this type conversion, we need to know the DBAL type
class for the root entity's `#[Id]`, and we have to figure it out based
on a given (arbitrary) DQL query. This requires DQL parsing and
inspecting the AST, so #7821 placed the conversion code in the
`WhereInWalker` where all the necessary information is available.

The problem is that type conversion has to happen every time the
paginator is run, but the query that results from running
`WhereInWalker` would be kept in the query cache. This was reported in
 #7837 and fixed by #7865, by making this particular query expire every
time. The query must not be cached, since the necessary ID type
conversion happens as a side-effect of running the `WhereInWalker`.

The Paginator internal query that uses `WhereInWalker` has its DQL
re-parsed and transformed in every request.

This PR moves the code that determines the DBAL type out of
`WhereInWalker` into a dedicated SQL walker class, `RootTypeWalker`.

`RootTypeWalker` uses a ~hack~  clever trick to report the type back: It
sets the type as the resulting "SQL" string. The benefit is that
`RootTypeWalker` results can be cached in the query cache themselves.
Only the first time a given DQL query has to be paginated, we need to
run this walker to find out the root entity's ID type. After that, the
type will be returned from the query cache.

With the type information being provided, `Paginator` can take care of
the necessary conversions by itself. This happens every time the
Paginator is used.

The internal query that uses `WhereInWalker` can be cached again since
it no longer has side effects.
2023-02-05 11:17:38 +01:00
Matthias Pigulla ee8269ea55 Fix #9095 by re-applying #9096
Since #10411 has been merged, no more need to specify all intermediate
abstract entity classes.

Thus, we can relax the schema validator check as requested in #9095. The
reasons given in #9142 no longer apply.

Fixes #9095
2023-02-05 11:13:21 +01:00
Grégoire Paris d038f23570 Use linebreaks 2023-02-05 11:11:21 +01:00
Alexander M. Turek 3843d7e0cc Make all data providers static (#10493) 2023-02-04 08:35:56 +00:00
Jan Nedbal d50ba2e248 Fix invalid phpdocs missing null (#10490) 2023-02-03 22:36:32 +00:00
Jan Nedbal 8debb92d78 Add forgotten exception throws (#10489) 2023-02-03 21:32:13 +00:00
Grégoire Paris 8ff7938e31 Hunt down invalid docblocks (#10476)
It is OK to ignore some of the errors we get, but not this one.
2023-01-31 21:12:00 +01:00
Grégoire Paris d6c0031d44 Merge pull request #10453 from mpdude/mapped-superclass-association
Add regression test for a to-many relationship on a base class & mapped superclass in the hierarchy
2023-01-28 11:12:16 +01:00
Alexander M. Turek c78f933e57 Psalm 5.6.0, PHPStan 1.9.14 (#10468) 2023-01-26 19:05:45 +01:00
Matthias Pigulla 80eb85beaa Add regression test for a to-many relationship on a base class & mapped superclass in the hierarchy
This picks the test case from #9517 and rebases it onto 2.14.x.

The problem has been covered in #8415, so this PR closes #9517 and fixes #9516.

Co-authored-by: Robert D'Ercole <bobdercole@gmail.com>
2023-01-24 20:47:41 +00:00
Alexander M. Turek ed56f42cd5 Psalm 5.5.0 (#10445) 2023-01-23 17:33:19 +01:00
Matthias Pigulla 8b28543939 Avoid wasting Opcache memory with Paginator queries (#10434)
This PR prevents the Paginator from causing OpCache "wasted memory" to increase _on every request_ when used with Symfony's `PhpFilesAdapter` as the cache implementation for the query cache.

Depending on configured thresholds, wasted memory this will either cause periodic opcache restarts or running out of memory and not being able to cache additional scripts ([Details](https://tideways.com/profiler/blog/fine-tune-your-opcache-configuration-to-avoid-caching-suprises)).

Fixes #9917, closes #10095.

There is a long story (#7820, #7821, #7837, #7865) behind how the Paginator can take care of DBAL type conversions when creating the pagination query. This conversion has to transform identifier values before they will be used as a query parameter, so it has to happen every time the Paginator is used.

For reasons, this conversion happens inside `WhereInWalker`. Tree walkers like this are used only during the DQL parsing/AST processing steps. Having a DQL query in the query cache short-cuts this step by fetching the parsing/processing result from the cache.

So, to make sure the conversion happens also with the query cache being enabled, this line

https://github.com/doctrine/orm/blob/1753d035005c1125c9fb4855c3fa629341e5734d/lib/Doctrine/ORM/Tools/Pagination/Paginator.php#L165

was added in #7837. It causes `\Doctrine\ORM\Query::parse()` to re-parse the query every time, but will also put the result into the query cache afterwards.

At this point, the setup described in #9917 – which, to my knowledge, is the default in Symfony + DoctrineBundle projects – will ultimately bring us to this code:

https://github.com/symfony/symfony/blob/4b3391725f2fc4a072e776974f00a992cbc70515/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php#L248-L249

When writing a cache item with an already existing key, the driver has to make sure the opcache will honor the changed PHP file. This is what causes _wasted memory_ to increase.

Instead of using `\Doctrine\ORM\Query::expireQueryCache()`, which will force `\Doctrine\ORM\Query::parse()` to parse the query again before putting it into the cache, use `\Doctrine\ORM\Query::useQueryCache(false)`. The subtle difference is the latter will not place the processed query in the cache in the first place.

A test case is added to check that repeated use of the paginator does not call the cache to update existing keys. That should suffice to make sure we're not running into the issue, while at the same time not complicating tests by using the `PhpFilesAdapter` directly.

Note that in order to observe the described issue in tests, you will need to use the `PhpFilesDriver` and also make sure that OpCache is enabled and also activated for `php-cli` (which is running the unit tests).

This particular subquery generated/used by the Paginator is not put into the query cache. The DQL parsing/to-SQL conversion has to happen _every time_ the Paginator is used.

This, however, was already the case before this PR. In other words, this PR only changes that we do not store/update the cached result every time, but instead completely omit caching the query.
2023-01-23 13:14:46 +01:00
Javier Spagnoletti eec3c42494 Replace hardcoded name with Command::getName() in output message from UpdateCommand (#10443) 2023-01-23 12:51:16 +01:00
Grégoire Paris bc394877bc Use the right property (#10441) 2023-01-22 14:04:12 +07:00
Grégoire Paris 1753d03500 Merge pull request #10433 from mpdude/re-enable-tests-7820
Make sure tests from #7837 are actually run
2023-01-21 10:02:34 +01:00
Grégoire Paris 7ce6d8d427 Merge pull request #10436 from greg0ire/update-baseline
Update Psalm baseline
2023-01-20 11:43:52 +01:00
Grégoire Paris a48d95c71d Update Psalm baseline 2023-01-20 11:42:11 +01:00
Matthias Pigulla aee1d33042 Review the documentation regarding entity inheritance (#10429)
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2023-01-19 19:59:28 +01:00
Matthias Pigulla 8da741ad75 Make sure tests from 7820 are actually run 2023-01-19 12:05:32 +00:00
Matthias Pigulla ba7387fd8c Fixup GH8127 test case (#10424)
* Fixup GH8127 test case

This removes the unnecessary "middle2" class and puts back in the effect of the data provider.

Both changes were accidentally committed when I was working on #10411 and just meant as experiments during debugging.

* Fix CS
2023-01-18 14:42:26 +01:00
Grégoire Paris a83e4f7978 Merge pull request #10418 from greg0ire/unique-bool
Use correct type for FieldMapping#unique
2023-01-18 07:37:15 +01:00
Grégoire Paris 4f335ab565 Merge pull request #10415 from greg0ire/maintain-psalm-xml
Remove ignore rules for fixed issues
2023-01-18 07:36:27 +01:00
Grégoire Paris f88b0032ad Use correct type for FieldMapping#unique
Looking at usages in the codebase, it is supposed to be at least
bool|string, but not string.
When dumping the value inside `getFieldMapping`, it is always a boolean.
2023-01-17 16:29:07 +01:00
Grégoire Paris 69c7791ba2 Merge pull request #8415 from mpdude/mapped-superclass-association-inheritance
Fix association handling when there is a MappedSuperclass in the middle of an inheritance hierarchy
2023-01-17 16:25:20 +01:00
Grégoire Paris 1090dbe9be Merge pull request #10411 from mpdude/discover-missing-subclasses
Fill in missing subclasses when loading ClassMetadata
2023-01-17 16:13:53 +01:00
Matthias Pigulla c46b604ed7 Fix CS 2023-01-17 14:40:55 +00:00
Matthias Pigulla 8d9ebeded8 Fix association handling when there is a MappedSuperclass in the middle of an inheritance hierarchy
This fixes two closely related bugs.

1. When inheriting a to-one association from a mapped superclass, update the `sourceEntity` class name to the current class only when the association is actually _declared_ in the mapped superclass.
2. Reject association types that are not allowed on mapped superclasses only when they are actually _declared_ in a mapped superclass, not when inherited from parent classes.

Currently, when a many-to-one association is inherited from a `MappedSuperclass`, mapping information will be updated so that the association has the current (inheriting) class as the source entity.

https://github.com/doctrine/orm/blob/2138cc93834cfae9cd3f86c991fa051a3129b693/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php#L384-L393

This was added in 7dc8ef1db9 for [DDC-671](https://github.com/doctrine/orm/issues/5181).

The reason for this is that a mapped superclass is not an entity itself and has no table.

So, in the database, associations can only be from the inheriting entities' tables towards the referred-to target. This is also the reason for the limitation that only to-one associations may be added in mapped superclasses, since for those the database foreign key can be placed on the table(s) of the inheriting entities (and there may be more than one child class).

Neither the decision to update the `sourceEntity` nor the validation check should be based on `$parent->isMappedSuperclass`.

This only works in the simple case where the class hierarchy is `Mapped Superclass → Entity`.

The check is wrong when we have an inheritance hierarchy set up and the class hierarchy is `Base Entity → Mapped Superclass → Child Entity`.

Bug 1: The association should keep the root entity as the source. After all, in a JTI, the root table will contain the foreign key, and we need to base joins on that table when traversing `FROM LeafClass l JOIN l.target`.

Bug 2: Do not reject the to-many association declared in the base class. It is ok to have the reverse (owning) side point back to the base entity, as it would be if there were no mapped superclasses at all. The mapped superclass does not declare, add or otherwise interfere with the to-many association at all.

Base the decision to change the `sourceEntity` on `$mapping['inherited']` being set. This field points to the topmost _parent entity_ class in the ancestry tree where the relationship mapping appears for the first time.

When it is not set, the current class is the first _entity_ class in the hierarchy with that association. Since we are inheriting the relation, it must have been added in a mapped superclass above, but was not yet present in the nearest parent entity class.

In that case, it may only be a to-one association and the source entity needs to be updated.

(See #10396 for a clarification of the semantics of `inherited`.)

Here is a simplified example of the class hierarchy.

See the two tests added for more details – one is for checking the correct usage of a to-one association against/with the base class in JTI. The other is to test that a to-many association on the base class is not rejected.

I am sure that there are other tests that (still) cover the update of `sourceEntity` is happening.

```php
/**
 * @Entity
 */
class AssociationTarget
{
    /**
     * @Column(type="integer")
     * @Id
     * @GeneratedValue
     */
    public $id;
}

/**
 * @Entity
 * @InheritanceType("JOINED")
 * @DiscriminatorColumn(name="discriminator", type="string")
 * @DiscriminatorMap({"1" = "BaseClass", "2" = "LeafClass"})
 */
class BaseClass
{
    /**
     * @Column(type="integer")
     * @Id
     * @GeneratedValue
     */
    public $id;

    /**
     * @ManyToOne(targetEntity="AssociationTarget")
     */
    public $target;
}

/**
 * @MappedSuperclass
 */
class MediumSuperclass extends BaseClass
{
}

/**
 * @Entity
 */
class LeafClass extends MediumSuperclass
{
}
```

When querying `FROM LeafClass l`, it should be possible to `JOIN l.target`. This currently leads to an SQL error because the SQL join will be made via `LeafClass.target_id` instead of `BaseClass.target_id`. `LeafClass` is considered the `sourceEntity` for the association – which is wrong–, and so the foreign key field is expected to be in the `LeafClass` table (using JTI here).

Fixes #5998, fixes #7825.

I have removed the abstract entity class, since it is not relevant for the issue and took the discussion off course. Also, the discriminator map now contains all classes.

Added the second variant of the bug, namely that a to-many association would wrongly be rejected in the same situation.
2023-01-17 14:37:40 +00:00
Grégoire Paris 55f9178e84 Remove ignore rules for fixed issues
The ArgumentTypeCoercion one is hiding issues we could address.
Addressing them will require changes that should go in the next minor,
so I'm moving them to the baseline instead. That way, we cannot
introduce more issues of this type in this file.
2023-01-17 15:34:10 +01:00
Matthias Pigulla 60955755e0 Remove outdated todo 2023-01-17 08:49:36 +00:00
Matthias Pigulla a8e979819a Include a test for DDC-6558 2023-01-16 21:13:30 +00:00
Matthias Pigulla 4e8e3ef30b Discover entity subclasses that need not be declared in the discriminator map 2023-01-16 20:33:25 +00:00
114 changed files with 3489 additions and 1943 deletions
@@ -116,13 +116,18 @@ jobs:
- "3@dev"
postgres-version:
- "15"
extension:
- pdo_pgsql
- pgsql
include:
- php-version: "8.0"
dbal-version: "2.13"
postgres-version: "14"
extension: pdo_pgsql
- php-version: "8.2"
dbal-version: "default"
postgres-version: "9.6"
extension: pdo_pgsql
services:
postgres:
@@ -146,6 +151,7 @@ jobs:
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-version }}"
extensions: "pgsql pdo_pgsql"
coverage: "pcov"
ini-values: "zend.assertions=1, apc.enable_cli=1"
+1
View File
@@ -15,5 +15,6 @@ vendor/
/tests/Doctrine/Performance/history.db
/.phpcs-cache
composer.lock
.phpunit.cache
.phpunit.result.cache
/*.phpunit.xml
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
convertDeprecationsToExceptions="true"
>
<php>
<ini name="error_reporting" value="-1" />
<var name="db_driver" value="pgsql"/>
<var name="db_host" value="localhost" />
<var name="db_user" value="postgres" />
<var name="db_password" value="postgres" />
<var name="db_dbname" value="doctrine_tests" />
<!-- necessary change for some CLI/console output test assertions -->
<env name="COLUMNS" value="120"/>
</php>
<testsuites>
<testsuite name="Doctrine DBAL Test Suite">
<directory>../../../tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">../../../lib/Doctrine</directory>
</whitelist>
</filter>
<groups>
<exclude>
<group>performance</group>
<group>locking_functional</group>
</exclude>
</groups>
</phpunit>
+4 -4
View File
@@ -42,14 +42,14 @@
"doctrine/annotations": "^1.13 || ^2",
"doctrine/coding-standard": "^9.0.2 || ^11.0",
"phpbench/phpbench": "^0.16.10 || ^1.0",
"phpstan/phpstan": "~1.4.10 || 1.9.8",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"phpstan/phpstan": "~1.4.10 || 1.10.6",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.1",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^4.4 || ^5.4 || ^6.0",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"vimeo/psalm": "4.30.0 || 5.4.0"
"vimeo/psalm": "4.30.0 || 5.9.0"
},
"conflict": {
"doctrine/annotations": "<1.13 || >= 3.0"
+1 -1
View File
@@ -21,7 +21,7 @@ these comparisons are always made **BY REFERENCE**. That means the following cha
#[Entity]
class Article
{
#[Column(type='datetime')]
#[Column(type: 'datetime')]
private DateTime $updated;
public function setUpdated(): void
+8 -8
View File
@@ -1386,6 +1386,14 @@ Is essentially the same as following:
.. configuration-block::
.. code-block:: attribute
<?php
/** One Product has One Shipment. */
#[OneToOne(targetEntity: Shipment::class)]
#[JoinColumn(name: 'shipment_id', referencedColumnName: 'id', nullable: false)]
private Shipment $shipment;
.. code-block:: annotation
<?php
@@ -1396,14 +1404,6 @@ Is essentially the same as following:
*/
private Shipment $shipment;
.. code-block:: attribute
<?php
/** One Product has One Shipment. */
#[OneToOne(targetEntity: Shipment::class)]
#[JoinColumn(name: 'shipment_id', referencedColumnName: 'id', nullable: false)]
private Shipment $shipment;
.. code-block:: xml
<doctrine-mapping>
+1 -1
View File
@@ -575,7 +575,7 @@ Example with partial indexes:
#[Index(name: "search_idx", columns: ["category"],
options: [
"where": "((category IS NOT NULL))"
"where" => "((category IS NOT NULL))"
]
)]
class ECommerceProduct
-2
View File
@@ -534,8 +534,6 @@ the above example with ``allocationSize=100`` Doctrine ORM would only
need to access the sequence once to generate the identifiers for
100 new entities.
*The default allocationSize for a @SequenceGenerator is currently 10.*
.. caution::
The allocationSize is detected by SchemaTool and
+4 -2
View File
@@ -19,11 +19,13 @@ especially what the strategies presented here provide help with.
.. note::
Having an SQL logger enabled when processing batches can have a serious impact on performance and resource usage.
To avoid that you should disable it in the DBAL configuration:
To avoid that you should remove the corresponding middleware.
To remove all middlewares, you can use this line:
.. code-block:: php
<?php
$em->getConnection()->getConfiguration()->setSQLLogger(null);
$em->getConnection()->getConfiguration()->setMiddlewares([]); // DBAL 3
$em->getConnection()->getConfiguration()->setSQLLogger(null); // DBAL 2
Bulk Inserts
------------
+1 -1
View File
@@ -38,7 +38,7 @@ upon insert:
private string $algorithm = "sha1";
/** @var self::STATUS_* */
private int $status = self:STATUS_DISABLED;
private int $status = self::STATUS_DISABLED;
}
.
+111 -79
View File
@@ -1,6 +1,9 @@
Inheritance Mapping
===================
This chapter explains the available options for mapping class
hierarchies.
Mapped Superclasses
-------------------
@@ -14,6 +17,10 @@ Mapped superclasses, just as regular, non-mapped classes, can
appear in the middle of an otherwise mapped inheritance hierarchy
(through Single Table Inheritance or Class Table Inheritance).
No database table will be created for a mapped superclass itself,
only for entity classes inheriting from it. Also, a mapped superclass
need not have an ``#[Id]`` property.
.. note::
A mapped superclass cannot be an entity, it is not query-able and
@@ -25,6 +32,19 @@ appear in the middle of an otherwise mapped inheritance hierarchy
For further support of inheritance, the single or
joined table inheritance features have to be used.
.. warning::
At least when using attributes or annotations to specify your mapping,
it _seems_ as if you could inherit from a base class that is neither
an entity nor a mapped superclass, but has properties with mapping configuration
on them that would also be used in the inheriting class.
This, however, is due to how the corresponding mapping
drivers work and what the PHP reflection API reports for inherited fields.
Such a configuration is explicitly not supported. To give just one example,
it will break for ``private`` properties.
.. note::
You may be tempted to use traits to mix mapped fields or relationships
@@ -90,14 +110,69 @@ for the entity subclass. All the mappings from the mapped
superclass were inherited to the subclass as if they had been
defined on that class directly.
Entity Inheritance
------------------
As soon as one entity class inherits from another entity class, either
directly, with a mapped superclass or other unmapped (also called
"transient") classes in between, these entities form an inheritance
hierarchy. The topmost entity class in this hierarchy is called the
root entity, and the hierarchy includes all entities that are
descendants of this root entity.
On the root entity class, ``#[InheritanceType]``,
``#[DiscriminatorColumn]`` and ``#[DiscriminatorMap]`` must be specified.
``#[InheritanceType]`` specifies one of the two available inheritance
mapping strategies that are explained in the following sections.
``#[DiscriminatorColumn]`` designates the so-called discriminator column.
This is an extra column in the table that keeps information about which
type from the hierarchy applies for a particular database row.
``#[DiscriminatorMap]`` declares the possible values for the discriminator
column and maps them to class names in the hierarchy. This discriminator map
has to declare all non-abstract entity classes that exist in that particular
inheritance hierarchy. That includes the root as well as any intermediate
entity classes, given they are not abstract.
The names of the classes in the discriminator map do not need to be fully
qualified if the classes are contained in the same namespace as the entity
class on which the discriminator map is applied.
If no discriminator map is provided, then the map is generated
automatically. The automatically generated discriminator map contains the
lowercase short name of each class as key.
.. note::
Automatically generating the discriminator map is very expensive
computation-wise. The mapping driver has to provide all classes
for which mapping configuration exists, and those have to be
loaded and checked against the current inheritance hierarchy
to see if they are part of it. The resulting map, however, can be kept
in the metadata cache.
Performance impact on to-one associations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There is a general performance consideration when using entity inheritance:
If the target-entity of a many-to-one or one-to-one association is part of
an inheritance hierarchy, it is preferable for performance reasons that it
be a leaf entity (ie. have no subclasses).
Otherwise, the ORM is currently unable to tell beforehand which entity class
will have to be used, and so no appropriate proxy instance can be created.
That means the referred-to entities will *always* be loaded eagerly, which
might even propagate to relationships of these entities (in the case of
self-referencing associations).
Single Table Inheritance
------------------------
`Single Table Inheritance <https://martinfowler.com/eaaCatalog/singleTableInheritance.html>`_
is an inheritance mapping strategy where all classes of a hierarchy
are mapped to a single database table. In order to distinguish
which row represents which type in the hierarchy a so-called
discriminator column is used.
is an inheritance mapping strategy where all classes of a hierarchy are
mapped to a single database table.
Example:
@@ -162,27 +237,9 @@ Example:
MyProject\Model\Employee:
type: entity
Things to note:
- The ``#[InheritanceType]`` and ``#[DiscriminatorColumn]`` must be
specified on the topmost class that is part of the mapped entity
hierarchy.
- The ``#[DiscriminatorMap]`` specifies which values of the
discriminator column identify a row as being of a certain type. In
the case above a value of "person" identifies a row as being of
type ``Person`` and "employee" identifies a row as being of type
``Employee``.
- All entity classes that is part of the mapped entity hierarchy
(including the topmost class) should be specified in the
``#[DiscriminatorMap]``. In the case above Person class included.
- The names of the classes in the discriminator map do not need to
be fully qualified if the classes are contained in the same
namespace as the entity class on which the discriminator map is
applied.
- If no discriminator map is provided, then the map is generated
automatically. The automatically generated discriminator map
contains the lowercase short name of each class as key.
In this example, the ``#[DiscriminatorMap]`` specifies that in the
discriminator column, a value of "person" identifies a row as being of type
``Person`` and employee" identifies a row as being of type ``Employee``.
Design-time considerations
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -198,17 +255,10 @@ Performance impact
This strategy is very efficient for querying across all types in
the hierarchy or for specific types. No table joins are required,
only a WHERE clause listing the type identifiers. In particular,
only a ``WHERE`` clause listing the type identifiers. In particular,
relationships involving types that employ this mapping strategy are
very performing.
There is a general performance consideration with Single Table
Inheritance: If the target-entity of a many-to-one or one-to-one
association is an STI entity, it is preferable for performance reasons that it
be a leaf entity in the inheritance hierarchy, (ie. have no subclasses).
Otherwise Doctrine *CANNOT* create proxy instances
of this entity and will *ALWAYS* load the entity eagerly.
SQL Schema considerations
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -216,7 +266,7 @@ For Single-Table-Inheritance to work in scenarios where you are
using either a legacy database schema or a self-written database
schema you have to make sure that all columns that are not in the
root entity but in any of the different sub-entities has to allow
null values. Columns that have NOT NULL constraints have to be on
null values. Columns that have ``NOT NULL`` constraints have to be on
the root entity of the single-table inheritance hierarchy.
Class Table Inheritance
@@ -226,10 +276,11 @@ Class Table Inheritance
is an inheritance mapping strategy where each class in a hierarchy
is mapped to several tables: its own table and the tables of all
parent classes. The table of a child class is linked to the table
of a parent class through a foreign key constraint. Doctrine ORM
implements this strategy through the use of a discriminator column
in the topmost table of the hierarchy because this is the easiest
way to achieve polymorphic queries with Class Table Inheritance.
of a parent class through a foreign key constraint.
The discriminator column is placed in the topmost table of the hierarchy,
because this is the easiest way to achieve polymorphic queries with Class
Table Inheritance.
Example:
@@ -253,24 +304,9 @@ Example:
// ...
}
Things to note:
- The ``#[InheritanceType]``, ``#[DiscriminatorColumn]`` and
``#[DiscriminatorMap]`` must be specified on the topmost class that is
part of the mapped entity hierarchy.
- The ``#[DiscriminatorMap]`` specifies which values of the
discriminator column identify a row as being of which type. In the
case above a value of "person" identifies a row as being of type
``Person`` and "employee" identifies a row as being of type
``Employee``.
- The names of the classes in the discriminator map do not need to
be fully qualified if the classes are contained in the same
namespace as the entity class on which the discriminator map is
applied.
- If no discriminator map is provided, then the map is generated
automatically. The automatically generated discriminator map
contains the lowercase short name of each class as key.
As before, the ``#[DiscriminatorMap]`` specifies that in the
discriminator column, a value of "person" identifies a row as being of type
``Person`` and "employee" identifies a row as being of type ``Employee``.
.. note::
@@ -301,20 +337,13 @@ perform just about any query which can have a negative impact on
performance, especially with large tables and/or large hierarchies.
When partial objects are allowed, either globally or on the
specific query, then querying for any type will not cause the
tables of subtypes to be OUTER JOINed which can increase
tables of subtypes to be ``OUTER JOIN``ed which can increase
performance but the resulting partial objects will not fully load
themselves on access of any subtype fields, so accessing fields of
subtypes after such a query is not safe.
There is a general performance consideration with Class Table
Inheritance: If the target-entity of a many-to-one or one-to-one
association is a CTI entity, it is preferable for performance reasons that it
be a leaf entity in the inheritance hierarchy, (ie. have no subclasses).
Otherwise Doctrine *CANNOT* create proxy instances
of this entity and will *ALWAYS* load the entity eagerly.
There is also another important performance consideration that it is *NOT POSSIBLE*
to query for the base entity without any LEFT JOINs to the sub-types.
There is also another important performance consideration that it is *not possible*
to query for the base entity without any ``LEFT JOIN``s to the sub-types.
SQL Schema considerations
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -332,14 +361,16 @@ column and cascading on delete.
Overrides
---------
Used to override a mapping for an entity field or relationship. Can only be
applied to an entity that extends a mapped superclass or uses a trait to
override a relationship or field mapping defined by the mapped superclass or
trait.
Overrides can only be applied to entities that extend a mapped superclass or
use traits. They are used to override a mapping for an entity field or
relationship defined in that mapped superclass or trait.
It is not possible to override attributes or associations in entity to entity
inheritance scenarios, because this can cause unforseen edge case behavior and
increases complexity in ORM internal classes.
It is not supported to use overrides in entity inheritance scenarios.
.. note::
When using traits, make sure not to miss the warnings given in the
:doc:`Limitations and Known Issues<reference/limitations-and-known-issues>` chapter.
Association Override
@@ -544,10 +575,11 @@ Example:
Things to note:
- The "association override" specifies the overrides base on the property name.
- This feature is available for all kind of associations. (OneToOne, OneToMany, ManyToOne, ManyToMany)
- The association type *CANNOT* be changed.
- The override could redefine the joinTables or joinColumns depending on the association type.
- The "association override" specifies the overrides based on the property
name.
- This feature is available for all kind of associations (OneToOne, OneToMany, ManyToOne, ManyToMany).
- The association type *cannot* be changed.
- The override could redefine the ``joinTables`` or ``joinColumns`` depending on the association type.
- The override could redefine ``inversedBy`` to reference more than one extended entity.
- The override could redefine fetch to modify the fetch strategy of the extended entity.
@@ -720,8 +752,8 @@ Could be used by an entity that extends a mapped superclass to override a field
Things to note:
- The "attribute override" specifies the overrides base on the property name.
- The column type *CANNOT* be changed. If the column type is not equal you get a ``MappingException``
- The "attribute override" specifies the overrides based on the property name.
- The column type *cannot* be changed. If the column type is not equal, you get a ``MappingException``.
- The override can redefine all the attributes except the type.
Query the Type
@@ -114,8 +114,8 @@ functionally equivalent to the previously shown code looks as follows:
The difference between ``Connection#transactional($func)`` and
``EntityManager#transactional($func)`` is that the latter
abstraction flushes the ``EntityManager`` prior to transaction
commit and rolls back the transaction when an
exception occurs.
commit and in case of an exception the ``EntityManager`` gets closed
in addition to the transaction rollback.
.. _transactions-and-concurrency_exception-handling:
+1 -1
View File
@@ -166,7 +166,7 @@ step:
$connection = DriverManager::getConnection([
'driver' => 'pdo_sqlite',
'path' => __DIR__ . '/db.sqlite',
], $config)
], $config);
// obtaining the entity manager
$entityManager = new EntityManager($connection, $config);
+10 -3
View File
@@ -302,7 +302,7 @@
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="type" type="xs:NMTOKEN" default="string" />
<xs:attribute name="type" type="orm:type" default="string" />
<xs:attribute name="column" type="orm:columntoken" />
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="unique" type="xs:boolean" default="false" />
@@ -414,7 +414,7 @@
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="type" type="xs:NMTOKEN" />
<xs:attribute name="type" type="orm:type" />
<xs:attribute name="column" type="orm:columntoken" />
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="association-key" type="xs:boolean" default="false" />
@@ -446,6 +446,13 @@
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="type" id="type">
<xs:restriction base="xs:token">
<xs:pattern value="([a-zA-Z_u01-uff][a-zA-Z0-9_u01-uff]+)|(\c+)" id="type.class.pattern">
</xs:pattern>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="inverse-join-columns">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="join-column" type="orm:join-column" minOccurs="1" maxOccurs="unbounded" />
@@ -630,7 +637,7 @@
<xs:element name="options" type="orm:options" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="type" type="xs:NMTOKEN" default="string" />
<xs:attribute name="type" type="orm:type" default="string" />
<xs:attribute name="column" type="orm:columntoken" />
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="unique" type="xs:boolean" default="false" />
@@ -246,7 +246,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
*
* @param string $query
* @param string[]|Criteria $criteria
* @param string[] $orderBy
* @param string[]|null $orderBy
* @param int|null $limit
* @param int|null $offset
*
+1 -1
View File
@@ -227,7 +227,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
$alias
);
} else {
NotSupported::createForPersistence3(sprintf(
throw NotSupported::createForPersistence3(sprintf(
'Using short namespace alias "%s" by calling %s',
$alias,
__METHOD__
+1 -1
View File
@@ -814,7 +814,7 @@ class EntityManager implements EntityManagerInterface
$entityName
);
} else {
NotSupported::createForPersistence3(sprintf(
throw NotSupported::createForPersistence3(sprintf(
'Using short namespace alias "%s" when calling %s',
$entityName,
__METHOD__
@@ -51,7 +51,7 @@ final class HydrationCompleteHandler
}
/**
* This method should me called after any hydration cycle completed.
* This method should be called after any hydration cycle completed.
*
* Method fires all deferred invocations of postLoad events
*/
@@ -115,6 +115,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
$class->setVersioned($parent->isVersioned);
$class->setVersionField($parent->versionField);
$class->setDiscriminatorMap($parent->discriminatorMap);
$class->addSubClasses($parent->subClasses);
$class->setLifecycleCallbacks($parent->lifecycleCallbacks);
$class->setChangeTrackingPolicy($parent->changeTrackingPolicy);
@@ -219,11 +220,16 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
$this->addDefaultDiscriminatorMap($class);
}
// During the following event, there may also be updates to the discriminator map as per GH-1257/GH-8402.
// So, we must not discover the missing subclasses before that.
if ($this->evm->hasListeners(Events::loadClassMetadata)) {
$eventArgs = new LoadClassMetadataEventArgs($class, $this->em);
$this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs);
}
$this->findAbstractEntityClassesNotListedInDiscriminatorMap($class);
if ($class->changeTrackingPolicy === ClassMetadata::CHANGETRACKING_NOTIFY) {
Deprecation::trigger(
'doctrine/orm',
@@ -338,6 +344,57 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
$class->setDiscriminatorMap($map);
}
private function findAbstractEntityClassesNotListedInDiscriminatorMap(ClassMetadata $rootEntityClass): void
{
// Only root classes in inheritance hierarchies need contain a discriminator map,
// so skip for other classes.
if (! $rootEntityClass->isRootEntity() || $rootEntityClass->isInheritanceTypeNone()) {
return;
}
$processedClasses = [$rootEntityClass->name => true];
foreach ($rootEntityClass->subClasses as $knownSubClass) {
$processedClasses[$knownSubClass] = true;
}
foreach ($rootEntityClass->discriminatorMap as $declaredClassName) {
// This fetches non-transient parent classes only
$parentClasses = $this->getParentClasses($declaredClassName);
foreach ($parentClasses as $parentClass) {
if (isset($processedClasses[$parentClass])) {
continue;
}
$processedClasses[$parentClass] = true;
// All non-abstract entity classes must be listed in the discriminator map, and
// this will be validated/enforced at runtime (possibly at a later time, when the
// subclass is loaded, but anyways). Also, subclasses is about entity classes only.
// That means we can ignore non-abstract classes here. The (expensive) driver
// check for mapped superclasses need only be run for abstract candidate classes.
if (! (new ReflectionClass($parentClass))->isAbstract() || $this->peekIfIsMappedSuperclass($parentClass)) {
continue;
}
// We have found a non-transient, non-mapped-superclass = an entity class (possibly abstract, but that does not matter)
$rootEntityClass->addSubClass($parentClass);
}
}
}
/** @param class-string $className */
private function peekIfIsMappedSuperclass(string $className): bool
{
$reflService = $this->getReflectionService();
$class = $this->newClassMetadataInstance($className);
$this->initializeReflection($class, $reflService);
$this->driver->loadMetadataForClass($className, $class);
return $class->isMappedSuperclass;
}
/**
* Gets the lower-case short name of a class.
*
@@ -384,14 +441,6 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $parentClass): void
{
foreach ($parentClass->associationMappings as $field => $mapping) {
if ($parentClass->isMappedSuperclass) {
if ($mapping['type'] & ClassMetadata::TO_MANY && ! $mapping['isOwningSide']) {
throw MappingException::illegalToManyAssociationOnMappedSuperclass($parentClass->name, $field);
}
$mapping['sourceEntity'] = $subClass->name;
}
if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
$mapping['inherited'] = $parentClass->name;
}
@@ -400,6 +449,20 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
$mapping['declared'] = $parentClass->name;
}
// When the class inheriting the relation ($subClass) is the first entity class since the
// relation has been defined in a mapped superclass (or in a chain
// of mapped superclasses) above, then declare this current entity class as the source of
// the relationship.
// According to the definitions given in https://github.com/doctrine/orm/pull/10396/,
// this is the case <=> ! isset($mapping['inherited']).
if (! isset($mapping['inherited'])) {
if ($mapping['type'] & ClassMetadata::TO_MANY && ! $mapping['isOwningSide']) {
throw MappingException::illegalToManyAssociationOnMappedSuperclass($parentClass->name, $field);
}
$mapping['sourceEntity'] = $subClass->name;
}
$subClass->addInheritedAssociationMapping($mapping);
}
}
+40 -25
View File
@@ -82,7 +82,7 @@ use const PHP_VERSION_ID;
* columnDefinition?: string,
* precision?: int,
* scale?: int,
* unique?: string,
* unique?: bool,
* inherited?: class-string,
* originalClass?: class-string,
* originalField?: string,
@@ -397,7 +397,27 @@ class ClassMetadataInfo implements ClassMetadata
public $parentClasses = [];
/**
* READ-ONLY: The names of all subclasses (descendants).
* READ-ONLY: For classes in inheritance mapping hierarchies, this field contains the names of all
* <em>entity</em> subclasses of this class. These may also be abstract classes.
*
* This list is used, for example, to enumerate all necessary tables in JTI when querying for root
* or subclass entities, or to gather all fields comprised in an entity inheritance tree.
*
* For classes that do not use STI/JTI, this list is empty.
*
* Implementation note:
*
* In PHP, there is no general way to discover all subclasses of a given class at runtime. For that
* reason, the list of classes given in the discriminator map at the root entity is considered
* authoritative. The discriminator map must contain all <em>concrete</em> classes that can
* appear in the particular inheritance hierarchy tree. Since there can be no instances of abstract
* entity classes, users are not required to list such classes with a discriminator value.
*
* The possibly remaining "gaps" for abstract entity classes are filled after the class metadata for the
* root entity has been loaded.
*
* For subclasses of such root entities, the list can be reused/passed downwards, it only needs to
* be filtered accordingly (only keep remaining subclasses)
*
* @psalm-var list<class-string>
*/
@@ -536,7 +556,7 @@ class ClassMetadataInfo implements ClassMetadata
* - <b>scale</b> (integer, optional, schema-only)
* The scale of a decimal column. Only valid if the column type is decimal.
*
* - <b>'unique'</b> (string, optional, schema-only)
* - <b>'unique'</b> (boolean, optional, schema-only)
* Whether a unique constraint should be generated for the column.
*
* - <b>'inherited'</b> (string, optional)
@@ -1333,7 +1353,7 @@ class ClassMetadataInfo implements ClassMetadata
/**
* @param string $fieldName
* @param array $cache
* @psalm-param array{usage?: int, region?: string|null} $cache
* @psalm-param array{usage?: int|null, region?: string|null} $cache
*
* @return int[]|string[]
* @psalm-return array{usage: int, region: string|null}
@@ -1773,25 +1793,7 @@ class ClassMetadataInfo implements ClassMetadata
* @psalm-param array<string, mixed> $mapping The mapping.
*
* @return mixed[] The updated mapping.
* @psalm-return array{
* mappedBy: mixed|null,
* inversedBy: mixed|null,
* isOwningSide: bool,
* sourceEntity: class-string,
* targetEntity: string,
* fieldName: mixed,
* fetch: mixed,
* cascade: array<array-key,string>,
* isCascadeRemove: bool,
* isCascadePersist: bool,
* isCascadeRefresh: bool,
* isCascadeMerge: bool,
* isCascadeDetach: bool,
* type: int,
* originalField: string,
* originalClass: class-string,
* ?orphanRemoval: bool
* }
* @psalm-return AssociationMapping
*
* @throws MappingException If something is wrong with the mapping.
*/
@@ -1921,7 +1923,6 @@ class ClassMetadataInfo implements ClassMetadata
* @psalm-param array<string, mixed> $mapping The mapping to validate & complete.
*
* @return mixed[] The validated & completed mapping.
* @psalm-return array{isOwningSide: mixed, orphanRemoval: bool, isCascadeRemove: bool}
* @psalm-return array{
* mappedBy: mixed|null,
* inversedBy: mixed|null,
@@ -2076,7 +2077,6 @@ class ClassMetadataInfo implements ClassMetadata
* Validates & completes a many-to-many association mapping.
*
* @psalm-param array<string, mixed> $mapping The mapping to validate & complete.
* @psalm-param array<string, mixed> $mapping The mapping to validate & complete.
*
* @return mixed[] The validated & completed mapping.
* @psalm-return array{
@@ -3324,6 +3324,21 @@ class ClassMetadataInfo implements ClassMetadata
throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
}
$this->addSubClass($className);
}
/** @param array<class-string> $classes */
public function addSubClasses(array $classes): void
{
foreach ($classes as $className) {
$this->addSubClass($className);
}
}
public function addSubClass(string $className): void
{
// By ignoring classes that are not subclasses of the current class, we simplify inheriting
// the subclass list from a parent class at the beginning of \Doctrine\ORM\Mapping\ClassMetadataFactory::doLoadMetadata.
if (is_subclass_of($className, $this->name) && ! in_array($className, $this->subClasses, true)) {
$this->subClasses[] = $className;
}
@@ -16,10 +16,10 @@ class SimplifiedXmlDriver extends XmlDriver
/**
* {@inheritDoc}
*/
public function __construct($prefixes, $fileExtension = self::DEFAULT_FILE_EXTENSION)
public function __construct($prefixes, $fileExtension = self::DEFAULT_FILE_EXTENSION, bool $isXsdValidationEnabled = false)
{
$locator = new SymfonyFileLocator((array) $prefixes, $fileExtension);
parent::__construct($locator, $fileExtension);
parent::__construct($locator, $fileExtension, $isXsdValidationEnabled);
}
}
@@ -903,11 +903,10 @@ class YamlDriver extends FileDriver
* Parse / Normalize the cache configuration
*
* @param mixed[] $cacheMapping
* @psalm-param array{usage: mixed, region: (string|null)} $cacheMapping
* @psalm-param array{usage: string, region?: string} $cacheMapping
* @psalm-param array{usage: string|null, region?: mixed} $cacheMapping
*
* @return mixed[]
* @psalm-return array{usage: int, region: string|null}
* @psalm-return array{usage: int|null, region: string|null}
*/
private function cacheToArray(array $cacheMapping): array
{
+3 -3
View File
@@ -46,9 +46,9 @@ final class Table implements MappingAttribute
public $options = [];
/**
* @param array<Index> $indexes
* @param array<UniqueConstraint> $uniqueConstraints
* @param array<string,mixed> $options
* @param array<Index>|null $indexes
* @param array<UniqueConstraint>|null $uniqueConstraints
* @param array<string,mixed> $options
*/
public function __construct(
?string $name = null,
@@ -40,9 +40,9 @@ final class UniqueConstraint implements MappingAttribute
public $options;
/**
* @param array<string> $columns
* @param array<string> $fields
* @param array<string,mixed> $options
* @param array<string>|null $columns
* @param array<string>|null $fields
* @param array<string,mixed>|null $options
*/
public function __construct(
?string $name = null,
@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Persisters\Collection;
use BadMethodCallException;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
@@ -246,10 +247,15 @@ class ManyToManyPersister extends AbstractCollectionPersister
foreach ($parameters as $parameter) {
[$name, $value, $operator] = $parameter;
$field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform);
$whereClauses[] = sprintf('te.%s %s ?', $field, $operator);
$params[] = $value;
$paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0];
$field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform);
if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
$whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT');
} else {
$whereClauses[] = sprintf('te.%s %s ?', $field, $operator);
$params[] = $value;
$paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0];
}
}
$tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
@@ -890,15 +890,17 @@ class BasicEntityPersister implements EntityPersister
$valueVisitor->dispatch($expression);
[$params, $types] = $valueVisitor->getParamsAndTypes();
foreach ($params as $param) {
$sqlParams = array_merge($sqlParams, $this->getValues($param));
}
[, $types] = $valueVisitor->getParamsAndTypes();
foreach ($types as $type) {
[$field, $value] = $type;
$sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
[$field, $value, $operator] = $type;
if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
continue;
}
$sqlParams = array_merge($sqlParams, $this->getValues($value));
$sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
}
return [$sqlParams, $sqlTypes];
@@ -27,18 +27,10 @@ class SqlValueVisitor extends ExpressionVisitor
*/
public function walkComparison(Comparison $comparison)
{
$value = $this->getValueFromComparison($comparison);
$field = $comparison->getField();
$operator = $comparison->getOperator();
if (($operator === Comparison::EQ || $operator === Comparison::IS) && $value === null) {
return null;
} elseif ($operator === Comparison::NEQ && $value === null) {
return null;
}
$value = $this->getValueFromComparison($comparison);
$this->values[] = $value;
$this->types[] = [$field, $value, $operator];
$this->types[] = [$comparison->getField(), $value, $comparison->getOperator()];
return null;
}
+112
View File
@@ -258,6 +258,8 @@ class SqlWalker implements TreeWalker
* @psalm-param QueryComponent $queryComponent
*
* @return void
*
* @not-deprecated
*/
public function setQueryComponent($dqlAlias, array $queryComponent)
{
@@ -276,6 +278,8 @@ class SqlWalker implements TreeWalker
* @param AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST
*
* @return Exec\AbstractSqlExecutor
*
* @not-deprecated
*/
public function getExecutor($AST)
{
@@ -628,6 +632,8 @@ class SqlWalker implements TreeWalker
* @param string $identVariable
*
* @return string
*
* @not-deprecated
*/
public function walkEntityIdentificationVariable($identVariable)
{
@@ -649,6 +655,8 @@ class SqlWalker implements TreeWalker
* @param string $fieldName
*
* @return string The SQL.
*
* @not-deprecated
*/
public function walkIdentificationVariable($identificationVariable, $fieldName = null)
{
@@ -670,6 +678,8 @@ class SqlWalker implements TreeWalker
* @param AST\PathExpression $pathExpr
*
* @return string
*
* @not-deprecated
*/
public function walkPathExpression($pathExpr)
{
@@ -731,6 +741,8 @@ class SqlWalker implements TreeWalker
* @param AST\SelectClause $selectClause
*
* @return string
*
* @not-deprecated
*/
public function walkSelectClause($selectClause)
{
@@ -854,6 +866,8 @@ class SqlWalker implements TreeWalker
* @param AST\FromClause $fromClause
*
* @return string
*
* @not-deprecated
*/
public function walkFromClause($fromClause)
{
@@ -873,6 +887,8 @@ class SqlWalker implements TreeWalker
* @param AST\IdentificationVariableDeclaration $identificationVariableDecl
*
* @return string
*
* @not-deprecated
*/
public function walkIdentificationVariableDeclaration($identificationVariableDecl)
{
@@ -895,6 +911,8 @@ class SqlWalker implements TreeWalker
* @param AST\IndexBy $indexBy
*
* @return void
*
* @not-deprecated
*/
public function walkIndexBy($indexBy)
{
@@ -948,6 +966,8 @@ class SqlWalker implements TreeWalker
* @param AST\RangeVariableDeclaration $rangeVariableDeclaration
*
* @return string
*
* @not-deprecated
*/
public function walkRangeVariableDeclaration($rangeVariableDeclaration)
{
@@ -998,6 +1018,8 @@ class SqlWalker implements TreeWalker
* @return string
*
* @throws QueryException
*
* @not-deprecated
*/
public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joinType = AST\Join::JOIN_TYPE_INNER, $condExpr = null)
{
@@ -1162,6 +1184,8 @@ class SqlWalker implements TreeWalker
* @param AST\Functions\FunctionNode $function
*
* @return string
*
* @not-deprecated
*/
public function walkFunction($function)
{
@@ -1174,6 +1198,8 @@ class SqlWalker implements TreeWalker
* @param AST\OrderByClause $orderByClause
*
* @return string
*
* @not-deprecated
*/
public function walkOrderByClause($orderByClause)
{
@@ -1193,6 +1219,8 @@ class SqlWalker implements TreeWalker
* @param AST\OrderByItem $orderByItem
*
* @return string
*
* @not-deprecated
*/
public function walkOrderByItem($orderByItem)
{
@@ -1217,6 +1245,8 @@ class SqlWalker implements TreeWalker
* @param AST\HavingClause $havingClause
*
* @return string The SQL.
*
* @not-deprecated
*/
public function walkHavingClause($havingClause)
{
@@ -1229,6 +1259,8 @@ class SqlWalker implements TreeWalker
* @param AST\Join $join
*
* @return string
*
* @not-deprecated
*/
public function walkJoin($join)
{
@@ -1291,6 +1323,8 @@ class SqlWalker implements TreeWalker
* @param AST\CoalesceExpression $coalesceExpression
*
* @return string The SQL.
*
* @not-deprecated
*/
public function walkCoalesceExpression($coalesceExpression)
{
@@ -1311,6 +1345,8 @@ class SqlWalker implements TreeWalker
* @param AST\NullIfExpression $nullIfExpression
*
* @return string The SQL.
*
* @not-deprecated
*/
public function walkNullIfExpression($nullIfExpression)
{
@@ -1329,6 +1365,8 @@ class SqlWalker implements TreeWalker
* Walks down a GeneralCaseExpression AST node and generates the corresponding SQL.
*
* @return string The SQL.
*
* @not-deprecated
*/
public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression)
{
@@ -1350,6 +1388,8 @@ class SqlWalker implements TreeWalker
* @param AST\SimpleCaseExpression $simpleCaseExpression
*
* @return string The SQL.
*
* @not-deprecated
*/
public function walkSimpleCaseExpression($simpleCaseExpression)
{
@@ -1371,6 +1411,8 @@ class SqlWalker implements TreeWalker
* @param AST\SelectExpression $selectExpression
*
* @return string
*
* @not-deprecated
*/
public function walkSelectExpression($selectExpression)
{
@@ -1575,6 +1617,8 @@ class SqlWalker implements TreeWalker
* @param AST\QuantifiedExpression $qExpr
*
* @return string
*
* @not-deprecated
*/
public function walkQuantifiedExpression($qExpr)
{
@@ -1587,6 +1631,8 @@ class SqlWalker implements TreeWalker
* @param AST\Subselect $subselect
*
* @return string
*
* @not-deprecated
*/
public function walkSubselect($subselect)
{
@@ -1616,6 +1662,8 @@ class SqlWalker implements TreeWalker
* @param AST\SubselectFromClause $subselectFromClause
*
* @return string
*
* @not-deprecated
*/
public function walkSubselectFromClause($subselectFromClause)
{
@@ -1635,6 +1683,8 @@ class SqlWalker implements TreeWalker
* @param AST\SimpleSelectClause $simpleSelectClause
*
* @return string
*
* @not-deprecated
*/
public function walkSimpleSelectClause($simpleSelectClause)
{
@@ -1733,6 +1783,8 @@ class SqlWalker implements TreeWalker
* @param AST\SimpleSelectExpression $simpleSelectExpression
*
* @return string
*
* @not-deprecated
*/
public function walkSimpleSelectExpression($simpleSelectExpression)
{
@@ -1788,6 +1840,8 @@ class SqlWalker implements TreeWalker
* @param AST\AggregateExpression $aggExpression
*
* @return string
*
* @not-deprecated
*/
public function walkAggregateExpression($aggExpression)
{
@@ -1801,6 +1855,8 @@ class SqlWalker implements TreeWalker
* @param AST\GroupByClause $groupByClause
*
* @return string
*
* @not-deprecated
*/
public function walkGroupByClause($groupByClause)
{
@@ -1819,6 +1875,8 @@ class SqlWalker implements TreeWalker
* @param AST\PathExpression|string $groupByItem
*
* @return string
*
* @not-deprecated
*/
public function walkGroupByItem($groupByItem)
{
@@ -1868,6 +1926,8 @@ class SqlWalker implements TreeWalker
* Walks down a DeleteClause AST node, thereby generating the appropriate SQL.
*
* @return string
*
* @not-deprecated
*/
public function walkDeleteClause(AST\DeleteClause $deleteClause)
{
@@ -1887,6 +1947,8 @@ class SqlWalker implements TreeWalker
* @param AST\UpdateClause $updateClause
*
* @return string
*
* @not-deprecated
*/
public function walkUpdateClause($updateClause)
{
@@ -1906,6 +1968,8 @@ class SqlWalker implements TreeWalker
* @param AST\UpdateItem $updateItem
*
* @return string
*
* @not-deprecated
*/
public function walkUpdateItem($updateItem)
{
@@ -1941,6 +2005,8 @@ class SqlWalker implements TreeWalker
* @param AST\WhereClause $whereClause
*
* @return string
*
* @not-deprecated
*/
public function walkWhereClause($whereClause)
{
@@ -1985,6 +2051,8 @@ class SqlWalker implements TreeWalker
* @param AST\ConditionalExpression $condExpr
*
* @return string
*
* @not-deprecated
*/
public function walkConditionalExpression($condExpr)
{
@@ -2003,6 +2071,8 @@ class SqlWalker implements TreeWalker
* @param AST\ConditionalTerm $condTerm
*
* @return string
*
* @not-deprecated
*/
public function walkConditionalTerm($condTerm)
{
@@ -2021,6 +2091,8 @@ class SqlWalker implements TreeWalker
* @param AST\ConditionalFactor $factor
*
* @return string The SQL.
*
* @not-deprecated
*/
public function walkConditionalFactor($factor)
{
@@ -2037,6 +2109,8 @@ class SqlWalker implements TreeWalker
* @param AST\ConditionalPrimary $primary
*
* @return string
*
* @not-deprecated
*/
public function walkConditionalPrimary($primary)
{
@@ -2057,6 +2131,8 @@ class SqlWalker implements TreeWalker
* @param AST\ExistsExpression $existsExpr
*
* @return string
*
* @not-deprecated
*/
public function walkExistsExpression($existsExpr)
{
@@ -2073,6 +2149,8 @@ class SqlWalker implements TreeWalker
* @param AST\CollectionMemberExpression $collMemberExpr
*
* @return string
*
* @not-deprecated
*/
public function walkCollectionMemberExpression($collMemberExpr)
{
@@ -2174,6 +2252,8 @@ class SqlWalker implements TreeWalker
* @param AST\EmptyCollectionComparisonExpression $emptyCollCompExpr
*
* @return string
*
* @not-deprecated
*/
public function walkEmptyCollectionComparisonExpression($emptyCollCompExpr)
{
@@ -2189,6 +2269,8 @@ class SqlWalker implements TreeWalker
* @param AST\NullComparisonExpression $nullCompExpr
*
* @return string
*
* @not-deprecated
*/
public function walkNullComparisonExpression($nullCompExpr)
{
@@ -2275,6 +2357,8 @@ class SqlWalker implements TreeWalker
* @return string
*
* @throws QueryException
*
* @not-deprecated
*/
public function walkInstanceOfExpression($instanceOfExpr)
{
@@ -2301,6 +2385,8 @@ class SqlWalker implements TreeWalker
* @param mixed $inParam
*
* @return string
*
* @not-deprecated
*/
public function walkInParameter($inParam)
{
@@ -2315,6 +2401,8 @@ class SqlWalker implements TreeWalker
* @param AST\Literal $literal
*
* @return string
*
* @not-deprecated
*/
public function walkLiteral($literal)
{
@@ -2339,6 +2427,8 @@ class SqlWalker implements TreeWalker
* @param AST\BetweenExpression $betweenExpr
*
* @return string
*
* @not-deprecated
*/
public function walkBetweenExpression($betweenExpr)
{
@@ -2360,6 +2450,8 @@ class SqlWalker implements TreeWalker
* @param AST\LikeExpression $likeExpr
*
* @return string
*
* @not-deprecated
*/
public function walkLikeExpression($likeExpr)
{
@@ -2399,6 +2491,8 @@ class SqlWalker implements TreeWalker
* @param AST\PathExpression $stateFieldPathExpression
*
* @return string
*
* @not-deprecated
*/
public function walkStateFieldPathExpression($stateFieldPathExpression)
{
@@ -2411,6 +2505,8 @@ class SqlWalker implements TreeWalker
* @param AST\ComparisonExpression $compExpr
*
* @return string
*
* @not-deprecated
*/
public function walkComparisonExpression($compExpr)
{
@@ -2437,6 +2533,8 @@ class SqlWalker implements TreeWalker
* @param AST\InputParameter $inputParam
*
* @return string
*
* @not-deprecated
*/
public function walkInputParameter($inputParam)
{
@@ -2460,6 +2558,8 @@ class SqlWalker implements TreeWalker
* @param AST\ArithmeticExpression $arithmeticExpr
*
* @return string
*
* @not-deprecated
*/
public function walkArithmeticExpression($arithmeticExpr)
{
@@ -2474,6 +2574,8 @@ class SqlWalker implements TreeWalker
* @param AST\SimpleArithmeticExpression $simpleArithmeticExpr
*
* @return string
*
* @not-deprecated
*/
public function walkSimpleArithmeticExpression($simpleArithmeticExpr)
{
@@ -2490,6 +2592,8 @@ class SqlWalker implements TreeWalker
* @param mixed $term
*
* @return string
*
* @not-deprecated
*/
public function walkArithmeticTerm($term)
{
@@ -2514,6 +2618,8 @@ class SqlWalker implements TreeWalker
* @param mixed $factor
*
* @return string
*
* @not-deprecated
*/
public function walkArithmeticFactor($factor)
{
@@ -2540,6 +2646,8 @@ class SqlWalker implements TreeWalker
* @param mixed $primary
*
* @return string The SQL.
*
* @not-deprecated
*/
public function walkArithmeticPrimary($primary)
{
@@ -2560,6 +2668,8 @@ class SqlWalker implements TreeWalker
* @param mixed $stringPrimary
*
* @return string
*
* @not-deprecated
*/
public function walkStringPrimary($stringPrimary)
{
@@ -2574,6 +2684,8 @@ class SqlWalker implements TreeWalker
* @param string $resultVariable
*
* @return string
*
* @not-deprecated
*/
public function walkResultVariable($resultVariable)
{
@@ -20,9 +20,7 @@ use function sprintf;
*/
class CollectionRegionCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:region:collection')
@@ -20,9 +20,7 @@ use function sprintf;
*/
class EntityRegionCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:region:entity')
@@ -18,9 +18,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
*/
class MetadataCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:metadata')
@@ -27,9 +27,7 @@ use function sprintf;
*/
class QueryCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:query')
@@ -20,9 +20,7 @@ use function sprintf;
*/
class QueryRegionCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:region:query')
@@ -29,9 +29,7 @@ use function sprintf;
*/
class ResultCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:clear-cache:result')
@@ -72,9 +72,7 @@ class ConvertDoctrine1SchemaCommand extends Command
$this->metadataExporter = $metadataExporter;
}
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:convert-d1-schema')
@@ -37,9 +37,7 @@ use function strtolower;
*/
class ConvertMappingCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:convert-mapping')
@@ -19,9 +19,7 @@ use Throwable;
*/
class EnsureProductionSettingsCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:ensure-production-settings')
@@ -28,9 +28,7 @@ use function sprintf;
*/
class GenerateEntitiesCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:generate-entities')
@@ -26,9 +26,7 @@ use function sprintf;
*/
class GenerateProxiesCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:generate-proxies')
@@ -27,9 +27,7 @@ use function sprintf;
*/
class GenerateRepositoriesCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:generate-repositories')
@@ -20,9 +20,7 @@ use function sprintf;
*/
class InfoCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:info')
@@ -27,9 +27,7 @@ use function strtoupper;
*/
class RunDqlCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:run-dql')
@@ -19,9 +19,7 @@ use function sprintf;
*/
class CreateCommand extends AbstractCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:schema-tool:create')
@@ -20,9 +20,7 @@ use function sprintf;
*/
class DropCommand extends AbstractCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:schema-tool:drop')
@@ -24,9 +24,7 @@ class UpdateCommand extends AbstractCommand
/** @var string */
protected $name = 'orm:schema-tool:update';
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName($this->name)
@@ -79,7 +77,10 @@ EOT
$saveMode = ! $input->getOption('complete');
if ($saveMode) {
$notificationUi->warning('Not passing the "--complete" option to "orm:schema-tool:update" is deprecated and will not be supported when using doctrine/dbal 4');
$notificationUi->warning(sprintf(
'Not passing the "--complete" option to "%s" is deprecated and will not be supported when using doctrine/dbal 4',
$this->getName() ?? $this->name
));
}
$sqls = $schemaTool->getUpdateSchemaSql($metadatas, $saveMode);
@@ -20,9 +20,7 @@ use function sprintf;
*/
class ValidateSchemaCommand extends AbstractEntityManagerCommand
{
/**
* {@inheritdoc}
*/
/** @return void */
protected function configure()
{
$this->setName('orm:validate-schema')
@@ -21,7 +21,8 @@ use Traversable;
use function array_key_exists;
use function array_map;
use function array_sum;
use function count;
use function assert;
use function is_string;
/**
* The paginator can handle various complex scenarios with DQL.
@@ -158,11 +159,12 @@ class Paginator implements Countable, IteratorAggregate
$ids = array_map('current', $foundIdRows);
$this->appendTreeWalker($whereInQuery, WhereInWalker::class);
$whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, count($ids));
$whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_HAS_IDS, true);
$whereInQuery->setFirstResult(0)->setMaxResults(null);
$whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $ids);
$whereInQuery->setCacheable($this->query->isCacheable());
$whereInQuery->expireQueryCache();
$databaseIds = $this->convertWhereInIdentifiersToDatabaseValues($ids);
$whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $databaseIds);
$result = $whereInQuery->getResult($this->query->getHydrationMode());
} else {
@@ -265,4 +267,23 @@ class Paginator implements Countable, IteratorAggregate
$query->setParameters($parameters);
}
/**
* @param mixed[] $identifiers
*
* @return mixed[]
*/
private function convertWhereInIdentifiersToDatabaseValues(array $identifiers): array
{
$query = $this->cloneQuery($this->query);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, RootTypeWalker::class);
$connection = $this->query->getEntityManager()->getConnection();
$type = $query->getSQL();
assert(is_string($type));
return array_map(static function ($id) use ($connection, $type) {
return $connection->convertToDatabaseValue($id, $type);
}, $identifiers);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Tools\Pagination;
use Doctrine\ORM\Query\AST;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Utility\PersisterHelper;
use RuntimeException;
use function count;
use function reset;
/**
* Infers the DBAL type of the #Id (identifier) column of the given query's root entity, and
* returns it in place of a real SQL statement.
*
* Obtaining this type is a necessary intermediate step for \Doctrine\ORM\Tools\Pagination\Paginator.
* We can best do this from a tree walker because it gives us access to the AST.
*
* Returning the type instead of a "real" SQL statement is a slight hack. However, it has the
* benefit that the DQL -> root entity id type resolution can be cached in the query cache.
*/
final class RootTypeWalker extends SqlWalker
{
public function walkSelectStatement(AST\SelectStatement $AST): string
{
// Get the root entity and alias from the AST fromClause
$from = $AST->fromClause->identificationVariableDeclarations;
if (count($from) > 1) {
throw new RuntimeException('Can only process queries that select only one FROM component');
}
$fromRoot = reset($from);
$rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
$rootClass = $this->getMetadataForDqlAlias($rootAlias);
$identifierFieldName = $rootClass->getSingleIdentifierFieldName();
return PersisterHelper::getTypeOfField(
$identifierFieldName,
$rootClass,
$this->getQuery()
->getEntityManager()
)[0];
}
}
@@ -17,13 +17,9 @@ use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\AST\SimpleArithmeticExpression;
use Doctrine\ORM\Query\AST\WhereClause;
use Doctrine\ORM\Query\TreeWalkerAdapter;
use Doctrine\ORM\Utility\PersisterHelper;
use RuntimeException;
use function array_map;
use function assert;
use function count;
use function is_array;
use function reset;
/**
@@ -32,15 +28,15 @@ use function reset;
* The parameter namespace (dpid) is defined by
* the PAGINATOR_ID_ALIAS
*
* The total number of parameters is retrieved from
* the HINT_PAGINATOR_ID_COUNT query hint.
* The HINT_PAGINATOR_HAS_IDS query hint indicates whether there are
* any ids in the parameter at all.
*/
class WhereInWalker extends TreeWalkerAdapter
{
/**
* ID Count hint name.
*/
public const HINT_PAGINATOR_ID_COUNT = 'doctrine.id.count';
public const HINT_PAGINATOR_HAS_IDS = 'doctrine.paginator_has_ids';
/**
* Primary key alias for query.
@@ -69,9 +65,9 @@ class WhereInWalker extends TreeWalkerAdapter
$pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $rootAlias, $identifierFieldName);
$pathExpression->type = $pathType;
$count = $this->_getQuery()->getHint(self::HINT_PAGINATOR_ID_COUNT);
$hasIds = $this->_getQuery()->getHint(self::HINT_PAGINATOR_HAS_IDS);
if ($count > 0) {
if ($hasIds) {
$arithmeticExpression = new ArithmeticExpression();
$arithmeticExpression->simpleArithmeticExpression = new SimpleArithmeticExpression(
[$pathExpression]
@@ -80,15 +76,6 @@ class WhereInWalker extends TreeWalkerAdapter
$arithmeticExpression,
[new InputParameter(':' . self::PAGINATOR_ID_ALIAS)]
);
$this->convertWhereInIdentifiersToDatabaseValue(
PersisterHelper::getTypeOfField(
$identifierFieldName,
$rootClass,
$this->_getQuery()
->getEntityManager()
)[0]
);
} else {
$expression = new NullComparisonExpression($pathExpression);
}
@@ -130,24 +117,4 @@ class WhereInWalker extends TreeWalkerAdapter
);
}
}
private function convertWhereInIdentifiersToDatabaseValue(string $type): void
{
$query = $this->_getQuery();
$identifiersParameter = $query->getParameter(self::PAGINATOR_ID_ALIAS);
assert($identifiersParameter !== null);
$identifiers = $identifiersParameter->getValue();
assert(is_array($identifiers));
$connection = $this->_getQuery()
->getEntityManager()
->getConnection();
$query->setParameter(self::PAGINATOR_ID_ALIAS, array_map(static function ($id) use ($connection, $type) {
return $connection->convertToDatabaseValue($id, $type);
}, $identifiers));
}
}
+4 -4
View File
@@ -97,7 +97,7 @@ class SchemaTool
foreach ($createSchemaSql as $sql) {
try {
$conn->executeQuery($sql);
$conn->executeStatement($sql);
} catch (Throwable $e) {
throw ToolsException::schemaToolFailure($sql, $e);
}
@@ -831,7 +831,7 @@ class SchemaTool
foreach ($dropSchemaSql as $sql) {
try {
$conn->executeQuery($sql);
$conn->executeStatement($sql);
} catch (Throwable $e) {
// ignored
}
@@ -849,7 +849,7 @@ class SchemaTool
$conn = $this->em->getConnection();
foreach ($dropSchemaSql as $sql) {
$conn->executeQuery($sql);
$conn->executeStatement($sql);
}
}
@@ -939,7 +939,7 @@ class SchemaTool
$conn = $this->em->getConnection();
foreach ($updateSchemaSql as $sql) {
$conn->executeQuery($sql);
$conn->executeStatement($sql);
}
}
+7 -1
View File
@@ -253,7 +253,13 @@ class SchemaValidator
}
}
if (! $class->isInheritanceTypeNone() && ! $class->isRootEntity() && ! $class->isMappedSuperclass && array_search($class->name, $class->discriminatorMap, true) === false) {
if (
! $class->isInheritanceTypeNone()
&& ! $class->isRootEntity()
&& ($class->reflClass !== null && ! $class->reflClass->isAbstract())
&& ! $class->isMappedSuperclass
&& array_search($class->name, $class->discriminatorMap, true) === false
) {
$ce[] = "Entity class '" . $class->name . "' is part of inheritance hierarchy, but is " .
"not mapped in the root entity '" . $class->rootEntityName . "' discriminator map. " .
'All subclasses must be listed in the discriminator map.';
+5 -15
View File
@@ -170,6 +170,11 @@ parameters:
count: 2
path: lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
-
message: "#^Method Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadataInfo\\:\\:_validateAndCompleteManyToManyMapping\\(\\) should return array\\{mappedBy\\: mixed, inversedBy\\: mixed, isOwningSide\\: bool, sourceEntity\\: class\\-string, targetEntity\\: string, fieldName\\: mixed, fetch\\: mixed, cascade\\: array\\<string\\>, \\.\\.\\.\\} but returns array\\{cache\\?\\: array, cascade\\: array\\<string\\>, declared\\?\\: class\\-string, fetch\\: mixed, fieldName\\: string, id\\?\\: bool, inherited\\?\\: class\\-string, indexBy\\?\\: string, \\.\\.\\.\\}\\.$#"
count: 1
path: lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
-
message: "#^Method Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadataInfo\\:\\:fullyQualifiedClassName\\(\\) should return class\\-string\\|null but returns string\\|null\\.$#"
count: 1
@@ -230,11 +235,6 @@ parameters:
count: 1
path: lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
-
message: "#^Offset 'usage' on array\\{usage\\: string, region\\?\\: string\\} in isset\\(\\) always exists and is not nullable\\.$#"
count: 1
path: lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php
-
message: "#^Call to function is_int\\(\\) with string will always evaluate to false\\.$#"
count: 1
@@ -420,11 +420,6 @@ parameters:
count: 1
path: lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php
-
message: "#^Access to an undefined property Doctrine\\\\ORM\\\\Query\\\\AST\\\\Node\\:\\:\\$pathExpression\\.$#"
count: 1
path: lib/Doctrine/ORM/Query/SqlWalker.php
-
message: "#^Call to function is_string\\(\\) with Doctrine\\\\ORM\\\\Query\\\\AST\\\\Node will always evaluate to false\\.$#"
count: 1
@@ -610,11 +605,6 @@ parameters:
count: 1
path: lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php
-
message: "#^Result of \\|\\| is always true\\.$#"
count: 1
path: lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php
-
message: "#^Else branch is unreachable because ternary operator condition is always true\\.$#"
count: 1
+1345 -1241
View File
File diff suppressed because it is too large Load Diff
+5 -12
View File
@@ -3,6 +3,8 @@
errorLevel="2"
phpVersion="8.2"
resolveFromConfigFile="true"
findUnusedBaselineEntry="true"
findUnusedCode="false"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
@@ -40,8 +42,6 @@
<referencedClass name="Doctrine\ORM\Tools\Console\Command\GenerateRepositoriesCommand"/>
<referencedClass name="Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper"/>
<referencedClass name="Doctrine\ORM\Tools\Console\EntityManagerProvider\HelperSetManagerProvider"/>
<!-- https://github.com/vimeo/psalm/issues/8617 -->
<referencedClass name="Doctrine\ORM\Mapping\Annotation"/>
</errorLevel>
</DeprecatedClass>
<DeprecatedConstant>
@@ -128,12 +128,6 @@
<file name="lib/Doctrine/ORM/Query/AST/InstanceOfExpression.php"/>
</errorLevel>
</InvalidParamDefault>
<InvalidReturnType>
<errorLevel type="suppress">
<!-- https://github.com/vimeo/psalm/issues/8819 -->
<file name="lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php"/>
</errorLevel>
</InvalidReturnType>
<MethodSignatureMismatch>
<errorLevel type="suppress">
<!-- See https://github.com/vimeo/psalm/issues/7357 -->
@@ -237,11 +231,10 @@
<referencedMethod name="Doctrine\DBAL\Platforms\AbstractPlatform::getGuidExpression"/>
</errorLevel>
</UndefinedMethod>
<ArgumentTypeCoercion>
<MissingClosureReturnType>
<errorLevel type="suppress">
<!-- See https://github.com/JetBrains/phpstorm-stubs/pull/1383 -->
<file name="lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php"/>
<file name="lib/Doctrine/ORM/Tools/Pagination/Paginator.php"/>
</errorLevel>
</ArgumentTypeCoercion>
</MissingClosureReturnType>
</issueHandlers>
</psalm>
@@ -134,7 +134,7 @@ class DDC964User
'fieldName' => 'address',
'targetEntity' => 'DDC964Address',
'cascade' => ['persist','merge'],
'joinColumn' => ['name' => 'address_id', 'referencedColumnMame' => 'id'],
'joinColumns' => [['name' => 'address_id', 'referencedColumnMame' => 'id']],
]
);
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH7717;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="gh7717_children")
*/
class GH7717Child
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
public ?int $id = null;
/**
* @ORM\Column(type="string", nullable=true)
*/
public ?string $nullableProperty = null;
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH7717;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="gh7717_parents")
*/
class GH7717Parent
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
public ?int $id = null;
/**
* @ORM\ManyToMany(targetEntity="GH7717Child", cascade={"persist"})
*
* @var Selectable<int, GH7717Child>
*/
public Selectable $children;
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Project;
class Project
{
/**
* @var string
*/
private $id;
/**
* @var string
*/
private $name;
public function __construct(string $id, string $name)
{
$this->id = $id;
$this->name = $name;
}
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Project;
class ProjectId
{
/**
* @var string
*/
private $id;
public function __construct(string $id)
{
$this->id = $id;
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Project;
class ProjectInvalidMapping
{
/**
* @var string
*/
private $id;
/**
* @var string
*/
private $name;
public function __construct(string $id, string $name)
{
$this->id = $id;
$this->name = $name;
}
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Project;
final class ProjectName
{
/**
* @var string
*/
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
@@ -83,7 +83,7 @@ final class AbstractQueryTest extends TestCase
}
/** @return array<string, array{string}> */
public function provideSettersWithDeprecatedDefault(): array
public static function provideSettersWithDeprecatedDefault(): array
{
return [
'setHydrationCacheProfile' => ['setHydrationCacheProfile'],
@@ -8,6 +8,7 @@ use Doctrine\Common\Annotations\SimpleAnnotationReader;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Cache\CacheConfiguration;
use Doctrine\ORM\Cache\Exception\MetadataCacheNotConfigured;
@@ -17,6 +18,7 @@ use Doctrine\ORM\Cache\Exception\QueryCacheUsesNonPersistentCache;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Exception\InvalidEntityRepository;
use Doctrine\ORM\Exception\NotSupported;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Exception\ProxyClassesAlwaysRegenerating;
use Doctrine\ORM\Mapping as AnnotationNamespace;
@@ -128,7 +130,11 @@ class ConfigurationTest extends DoctrineTestCase
public function testSetGetEntityNamespace(): void
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/8818');
if (class_exists(PersistentObject::class)) {
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/8818');
} else {
$this->expectException(NotSupported::class);
}
$this->configuration->addEntityNamespace('TestNamespace', __NAMESPACE__);
self::assertSame(__NAMESPACE__, $this->configuration->getEntityNamespace('TestNamespace'));
@@ -46,7 +46,7 @@ class EntityManagerDecoratorTest extends TestCase
}
/** @psalm-return Generator<string, mixed[]> */
public function getMethodParameters(): Generator
public static function getMethodParameters(): Generator
{
$class = new ReflectionClass(EntityManagerInterface::class);
@@ -55,12 +55,12 @@ class EntityManagerDecoratorTest extends TestCase
continue;
}
yield $method->getName() => $this->getParameters($method);
yield $method->getName() => self::getParameters($method);
}
}
/** @return mixed[] */
private function getParameters(ReflectionMethod $method): array
private static function getParameters(ReflectionMethod $method): array
{
/** Special case EntityManager::createNativeQuery() */
if ($method->getName() === 'createNativeQuery') {
@@ -198,7 +198,7 @@ class EntityManagerTest extends OrmTestCase
}
/** @return Generator<array{mixed}> */
public function dataToBeReturnedByWrapInTransaction(): Generator
public static function dataToBeReturnedByWrapInTransaction(): Generator
{
yield [[]];
yield [[1]];
@@ -384,7 +384,7 @@ EXCEPTION
}
/** @return array<string, array{class-string}> */
public function provideCardClasses(): array
public static function provideCardClasses(): array
{
return [
Card::class => [Card::class],
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\ORM\Query;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsAddressDTO;
@@ -13,6 +14,7 @@ use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\Models\CMS\CmsUserDTO;
use Doctrine\Tests\OrmFunctionalTestCase;
use function class_exists;
use function count;
/** @group DDC-1574 */
@@ -31,7 +33,7 @@ class NewOperatorTest extends OrmFunctionalTestCase
}
/** @psalm-return list<array{int}> */
public function provideDataForHydrationMode(): array
public static function provideDataForHydrationMode(): array
{
return [
[Query::HYDRATE_ARRAY],
@@ -208,6 +210,10 @@ class NewOperatorTest extends OrmFunctionalTestCase
public function testShouldSupportFromEntityNamespaceAlias(): void
{
if (! class_exists(PersistentObject::class)) {
self::markTestSkipped('This test requires doctrine/persistence 2');
}
$dql = '
SELECT
new CmsUserDTO(u.name, e.email, a.city)
@@ -235,6 +241,10 @@ class NewOperatorTest extends OrmFunctionalTestCase
public function testShouldSupportValueObjectNamespaceAlias(): void
{
if (! class_exists(PersistentObject::class)) {
self::markTestSkipped('This test requires doctrine/persistence 2');
}
$dql = '
SELECT
new cms:CmsUserDTO(u.name, e.email, a.city)
@@ -77,8 +77,8 @@ class OneToOneBidirectionalAssociationTest extends OrmFunctionalTestCase
public function testLazyLoadsObjectsOnTheOwningSide(): void
{
$this->createFixture();
$metadata = $this->_em->getClassMetadata(ECommerceCart::class);
$metadata->associationMappings['customer']['fetchMode'] = ClassMetadata::FETCH_LAZY;
$metadata = $this->_em->getClassMetadata(ECommerceCart::class);
$metadata->associationMappings['customer']['fetch'] = ClassMetadata::FETCH_LAZY;
$query = $this->_em->createQuery('select c from Doctrine\Tests\Models\ECommerce\ECommerceCart c');
$result = $query->getResult();
@@ -667,6 +667,28 @@ SQL
self::assertCount(9, $paginator->getIterator());
}
public function testDifferentResultLengthsDoNotRequireExtraQueryCacheEntries(): void
{
$dql = 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id >= :id';
$query = $this->_em->createQuery($dql);
$query->setMaxResults(10);
$query->setParameter('id', 1);
$paginator = new Paginator($query);
$initialResult = iterator_to_array($paginator->getIterator()); // exercise the Paginator
self::assertCount(9, $initialResult);
$initialQueryCount = count(self::$queryCache->getValues());
$query->setParameter('id', $initialResult[1]->id); // skip the first result element
$paginator = new Paginator($query);
self::assertCount(8, $paginator->getIterator()); // exercise the Paginator again, with a smaller result set
$newCount = count(self::$queryCache->getValues());
self::assertSame($initialQueryCount, $newCount);
}
public function populate(): void
{
$groups = [];
@@ -736,8 +758,17 @@ SQL
$this->_em->flush();
}
/** @psalm-return list<array{bool, bool}> */
public function useOutputWalkers(): array
/** @psalm-return list<array{bool}> */
public static function useOutputWalkers(): array
{
return [
[true],
[false],
];
}
/** @psalm-return list<array{bool}> */
public static function fetchJoinCollection(): array
{
return [
[true],
@@ -746,16 +777,7 @@ SQL
}
/** @psalm-return list<array{bool, bool}> */
public function fetchJoinCollection(): array
{
return [
[true],
[false],
];
}
/** @psalm-return list<array{bool, bool}> */
public function useOutputWalkersAndFetchJoinCollection(): array
public static function useOutputWalkersAndFetchJoinCollection(): array
{
return [
[true, false],
@@ -364,7 +364,7 @@ class QueryDqlFunctionTest extends OrmFunctionalTestCase
);
}
public function dateAddSubProvider(): array
public static function dateAddSubProvider(): array
{
$secondsInDay = 86400;
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\DBAL\Logging\Middleware as LoggingMiddleware;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\NonUniqueResultException;
@@ -557,6 +558,10 @@ class QueryTest extends OrmFunctionalTestCase
public function testSupportsQueriesWithEntityNamespaces(): void
{
if (! class_exists(PersistentObject::class)) {
self::markTestSkipped('This test requires doctrine/persistence 2');
}
$this->_em->getConfiguration()->addEntityNamespace('CMS', 'Doctrine\Tests\Models\CMS');
try {
@@ -77,7 +77,7 @@ class DDC2825Test extends OrmFunctionalTestCase
*
* @return string[][]
*/
public function getTestedClasses(): array
public static function getTestedClasses(): array
{
return [
[ExplicitSchemaAndTable::class, 'explicit_schema', 'explicit_table'],
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group DDC-6558
*/
class DDC6558Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->_schemaTool->createSchema([
$this->_em->getClassMetadata(DDC6558Person::class),
$this->_em->getClassMetadata(DDC6558Employee::class),
$this->_em->getClassMetadata(DDC6558Staff::class),
$this->_em->getClassMetadata(DDC6558Developer::class),
$this->_em->getClassMetadata(DDC6558Manager::class),
]);
}
public function testEmployeeIsPopulated(): void
{
$developer = new DDC6558Developer();
$developer->phoneNumber = 1231231231;
$developer->emailAddress = 'email@address.com';
$this->_em->persist($developer);
$this->_em->flush();
$this->_em->clear();
$persistedDeveloper = $this->_em->find(DDC6558Person::class, $developer->id);
self::assertNotNull($persistedDeveloper->phoneNumber);
self::assertNotNull($persistedDeveloper->emailAddress);
}
}
/**
* @ORM\Entity()
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="discr", type="string")
* @ORM\DiscriminatorMap({"manager" = "DDC6558Manager", "staff" = "DDC6558Staff", "developer" = "DDC6558Developer"})
*/
abstract class DDC6558Person
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
* @ORM\GeneratedValue()
*
* @var int
*/
public $id;
}
/** @ORM\Entity() */
class DDC6558Manager extends DDC6558Person
{
}
/**
* @ORM\Entity()
*/
abstract class DDC6558Employee extends DDC6558Person
{
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $phoneNumber;
}
/** @ORM\Entity() */
class DDC6558Staff extends DDC6558Employee
{
}
/** @ORM\Entity() */
class DDC6558Developer extends DDC6558Employee
{
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $emailAddress;
}
@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Tests\OrmTestCase;
use Generator;
use function array_map;
/**
* @group GH-10387
*/
class GH10387Test extends OrmTestCase
{
/**
* @dataProvider classHierachies
*/
public function testSchemaToolCreatesColumnForFieldInTheMiddleClass(array $classes): void
{
$em = $this->getTestEntityManager();
$schemaTool = new SchemaTool($em);
$metadata = array_map(static function (string $class) use ($em) {
return $em->getClassMetadata($class);
}, $classes);
$schema = $schemaTool->getSchemaFromMetadata([$metadata[0]]);
self::assertNotNull($schema->getTable('root')->getColumn('middle_class_field'));
self::assertNotNull($schema->getTable('root')->getColumn('leaf_class_field'));
}
public static function classHierachies(): Generator
{
yield 'hierarchy with Entity classes only' => [[GH10387EntitiesOnlyRoot::class, GH10387EntitiesOnlyMiddle::class, GH10387EntitiesOnlyLeaf::class]];
yield 'MappedSuperclass in the middle of the hierarchy' => [[GH10387MappedSuperclassRoot::class, GH10387MappedSuperclassMiddle::class, GH10387MappedSuperclassLeaf::class]];
yield 'abstract entity the the root and in the middle of the hierarchy' => [[GH10387AbstractEntitiesRoot::class, GH10387AbstractEntitiesMiddle::class, GH10387AbstractEntitiesLeaf::class]];
}
}
/**
* @ORM\Entity
* @ORM\Table(name="root")
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorMap({ "A": "GH10387EntitiesOnlyRoot", "B": "GH10387EntitiesOnlyMiddle", "C": "GH10387EntitiesOnlyLeaf"})
*/
class GH10387EntitiesOnlyRoot
{
/**
* @ORM\Id
* @ORM\Column
*
* @var string
*/
private $id;
}
/**
* @ORM\Entity
*/
class GH10387EntitiesOnlyMiddle extends GH10387EntitiesOnlyRoot
{
/**
* @ORM\Column(name="middle_class_field")
*
* @var string
*/
private $parentValue;
}
/**
* @ORM\Entity
*/
class GH10387EntitiesOnlyLeaf extends GH10387EntitiesOnlyMiddle
{
/**
* @ORM\Column(name="leaf_class_field")
*
* @var string
*/
private $childValue;
}
/**
* @ORM\Entity
* @ORM\Table(name="root")
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorMap({ "A": "GH10387MappedSuperclassRoot", "B": "GH10387MappedSuperclassLeaf"})
* ^- This DiscriminatorMap contains the Entity classes only, not the Mapped Superclass
*/
class GH10387MappedSuperclassRoot
{
/**
* @ORM\Id
* @ORM\Column
*
* @var string
*/
private $id;
}
/**
* @ORM\MappedSuperclass
*/
class GH10387MappedSuperclassMiddle extends GH10387MappedSuperclassRoot
{
/**
* @ORM\Column(name="middle_class_field")
*
* @var string
*/
private $parentValue;
}
/**
* @ORM\Entity
*/
class GH10387MappedSuperclassLeaf extends GH10387MappedSuperclassMiddle
{
/**
* @ORM\Column(name="leaf_class_field")
*
* @var string
*/
private $childValue;
}
/**
* @ORM\Entity
* @ORM\Table(name="root")
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorMap({ "A": "GH10387AbstractEntitiesLeaf"})
* ^- This DiscriminatorMap contains the single non-abstract Entity class only
*/
abstract class GH10387AbstractEntitiesRoot
{
/**
* @ORM\Id
* @ORM\Column
*
* @var string
*/
private $id;
}
/**
* @ORM\Entity
*/
abstract class GH10387AbstractEntitiesMiddle extends GH10387AbstractEntitiesRoot
{
/**
* @ORM\Column(name="middle_class_field")
*
* @var string
*/
private $parentValue;
}
/**
* @ORM\Entity
*/
class GH10387AbstractEntitiesLeaf extends GH10387AbstractEntitiesMiddle
{
/**
* @ORM\Column(name="leaf_class_field")
*
* @var string
*/
private $childValue;
}
@@ -0,0 +1,255 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group GH-5998
*/
class GH5998Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->_schemaTool->createSchema([
$this->_em->getClassMetadata(GH5998JTI::class),
$this->_em->getClassMetadata(GH5998JTIChild::class),
$this->_em->getClassMetadata(GH5998STI::class),
$this->_em->getClassMetadata(GH5998Basic::class),
$this->_em->getClassMetadata(GH5998Related::class),
]);
}
/**
* Verifies that MappedSuperclasses work within an inheritance hierarchy.
*/
public function testIssue(): void
{
// Test JTI
$this->classTests(GH5998JTIChild::class);
// Test STI
$this->classTests(GH5998STIChild::class);
// Test Basic
$this->classTests(GH5998Basic::class);
}
private function classTests($className): void
{
// Test insert
$child = new $className('Sam', 0, 1);
$child->rel = new GH5998Related();
$this->_em->persist($child);
$this->_em->persist($child->rel);
$this->_em->flush();
$this->_em->clear();
// Test find by rel
$child = $this->_em->getRepository($className)->findOneBy(['rel' => $child->rel]);
self::assertNotNull($child);
$this->_em->clear();
// Test query by id with fetch join
$child = $this->_em->createQuery('SELECT t, r FROM ' . $className . ' t JOIN t.rel r WHERE t.id = 1')->getOneOrNullResult();
self::assertNotNull($child);
// Test lock and update
$this->_em->transactional(static function ($em) use ($child): void {
$em->lock($child, LockMode::NONE);
$child->firstName = 'Bob';
$child->status = 0;
});
$this->_em->clear();
$child = $this->_em->getRepository($className)->find(1);
self::assertEquals($child->firstName, 'Bob');
self::assertEquals($child->status, 0);
// Test delete
$this->_em->remove($child);
$this->_em->flush();
$child = $this->_em->getRepository($className)->find(1);
self::assertNull($child);
}
}
/**
* @ORM\MappedSuperclass
*/
class GH5998Common
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity=GH5998Related::class)
* @ORM\JoinColumn(name="related_id", referencedColumnName="id")
*
* @var GH5998Related
*/
public $rel;
/**
* @ORM\Version
* @ORM\Column(type="integer")
*
* @var int
*/
public $version;
/** @var mixed */
public $other;
}
/**
* @ORM\Entity
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorMap({"child" = GH5998JTIChild::class})
*/
abstract class GH5998JTI extends GH5998Common
{
/**
* @ORM\Column(type="string", length=255)
*
* @var string
*/
public $firstName;
}
/**
* @ORM\MappedSuperclass
*/
class GH5998JTICommon extends GH5998JTI
{
/**
* @ORM\Column(type="integer")
*
* @var int
*/
public $status;
}
/**
* @ORM\Entity
*/
class GH5998JTIChild extends GH5998JTICommon
{
/**
* @ORM\Column(type="integer")
*
* @var int
*/
public $type;
public function __construct(string $firstName, int $type, int $status)
{
$this->firstName = $firstName;
$this->type = $type;
$this->status = $status;
}
}
/**
* @ORM\Entity
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorMap({"child" = GH5998STIChild::class})
*/
abstract class GH5998STI extends GH5998Common
{
/**
* @ORM\Column(type="string", length=255)
*
* @var string
*/
public $firstName;
}
/**
* @ORM\MappedSuperclass
*/
class GH5998STICommon extends GH5998STI
{
/**
* @ORM\Column(type="integer")
*
* @var int
*/
public $status;
}
/**
* @ORM\Entity
*/
class GH5998STIChild extends GH5998STICommon
{
/**
* @ORM\Column(type="integer")
*
* @var int
*/
public $type;
public function __construct(string $firstName, int $type, int $status)
{
$this->firstName = $firstName;
$this->type = $type;
$this->status = $status;
}
}
/**
* @ORM\Entity
*/
class GH5998Basic extends GH5998Common
{
/**
* @ORM\Column(type="string", length=255)
*
* @var string
*/
public $firstName;
/**
* @ORM\Column(type="integer")
*
* @var int
*/
public $status;
/**
* @ORM\Column(type="integer")
*
* @var int
*/
public $type;
public function __construct(string $firstName, int $type, int $status)
{
$this->firstName = $firstName;
$this->type = $type;
$this->status = $status;
}
}
/**
* @ORM\Entity()
*/
class GH5998Related
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
}
+45
View File
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Tests\Models\GH7717\GH7717Child;
use Doctrine\Tests\Models\GH7717\GH7717Parent;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @requires PHP 7.4
*/
final class GH7717Test extends OrmFunctionalTestCase
{
public function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH7717Parent::class,
GH7717Child::class
);
}
public function testManyToManyPersisterIsNullComparison(): void
{
$childWithNullProperty = new GH7717Child();
$childWithoutNullProperty = new GH7717Child();
$childWithoutNullProperty->nullableProperty = 'nope';
$parent = new GH7717Parent();
$parent->children = new ArrayCollection([$childWithNullProperty, $childWithoutNullProperty]);
$this->_em->persist($parent);
$this->_em->flush();
$this->_em->clear();
$parent = $this->_em->find(GH7717Parent::class, 1);
$this->assertCount(1, $parent->children->matching(new Criteria(Criteria::expr()->isNull('nullableProperty'))));
}
}
@@ -13,6 +13,10 @@ use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Assert;
use Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\CacheItem;
use function array_map;
use function is_string;
@@ -69,48 +73,85 @@ class GH7820Test extends OrmFunctionalTestCase
public function testWillFindSongsInPaginator(): void
{
$query = $this->_em->getRepository(GH7820Line::class)
->createQueryBuilder('l')
->orderBy('l.lineNumber', Criteria::ASC);
$lines = $this->fetchSongLinesWithPaginator();
self::assertSame(
self::SONG,
array_map(static function (GH7820Line $line): string {
return $line->toString();
}, iterator_to_array(new Paginator($query)))
);
self::assertSame(self::SONG, $lines);
}
/** @group GH7837 */
public function testWillFindSongsInPaginatorEvenWithCachedQueryParsing(): void
{
// Enable the query cache
$this->_em->getConfiguration()
->getQueryCache()
->clear();
// Fetch song lines with the paginator, also priming the query cache
$lines = $this->fetchSongLinesWithPaginator();
self::assertSame(self::SONG, $lines, 'Expected to return expected data before query cache is populated with DQL -> SQL translation. Were SQL parameters translated?');
// Fetch song lines again
$lines = $this->fetchSongLinesWithPaginator();
self::assertSame(self::SONG, $lines, 'Expected to return expected data even when DQL -> SQL translation is present in cache. Were SQL parameters translated again?');
}
public function testPaginatorDoesNotForceCacheToUpdateEntries(): void
{
$this->_em->getConfiguration()->setQueryCache(new class extends ArrayAdapter {
public function save(CacheItemInterface $item): bool
{
Assert::assertFalse($this->hasItem($item->getKey()), 'The cache should not have to overwrite the entry');
return parent::save($item);
}
});
// "Prime" the cache (in fact, that should not even happen)
$this->fetchSongLinesWithPaginator();
// Make sure we can query again without overwriting the cache
$this->fetchSongLinesWithPaginator();
}
public function testPaginatorQueriesWillBeCached(): void
{
$cache = new class extends ArrayAdapter {
/** @var bool */
private $failOnCacheMiss = false;
public function failOnCacheMiss(): void
{
$this->failOnCacheMiss = true;
}
public function getItem($key): CacheItem
{
$item = parent::getItem($key);
Assert::assertTrue(! $this->failOnCacheMiss || $item->isHit(), 'cache was missed');
return $item;
}
};
$this->_em->getConfiguration()->setQueryCache($cache);
// Prime the cache
$this->fetchSongLinesWithPaginator();
$cache->failOnCacheMiss();
$this->fetchSongLinesWithPaginator();
}
private function fetchSongLinesWithPaginator(): array
{
$query = $this->_em->getRepository(GH7820Line::class)
->createQueryBuilder('l')
->orderBy('l.lineNumber', Criteria::ASC);
->orderBy('l.lineNumber', Criteria::ASC)
->setMaxResults(100);
self::assertSame(
self::SONG,
array_map(static function (GH7820Line $line): string {
return $line->toString();
}, iterator_to_array(new Paginator($query))),
'Expected to return expected data before query cache is populated with DQL -> SQL translation. Were SQL parameters translated?'
);
$query = $this->_em->getRepository(GH7820Line::class)
->createQueryBuilder('l')
->orderBy('l.lineNumber', Criteria::ASC);
self::assertSame(
self::SONG,
array_map(static function (GH7820Line $line): string {
return $line->toString();
}, iterator_to_array(new Paginator($query))),
'Expected to return expected data even when DQL -> SQL translation is present in cache. Were SQL parameters translated again?'
);
return array_map(static function (GH7820Line $line): string {
return $line->toString();
}, iterator_to_array(new Paginator($query)));
}
}
@@ -75,7 +75,7 @@ final class GH7875Test extends OrmFunctionalTestCase
}
/** @return array<array<string|callable|null>> */
public function provideUpdateSchemaSqlWithSchemaAssetFilter(): array
public static function provideUpdateSchemaSqlWithSchemaAssetFilter(): array
{
return [
['/^(?!my_enti)/', null],
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH8127Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH8127Root::class,
GH8127Middle::class,
GH8127Leaf::class
);
}
/**
* @dataProvider queryClasses
*/
public function testLoadFieldsFromAllClassesInHierarchy(string $queryClass): void
{
$entity = new GH8127Leaf();
$entity->root = 'root';
$entity->middle = 'middle';
$entity->leaf = 'leaf';
$this->_em->persist($entity);
$this->_em->flush();
$this->_em->clear();
$loadedEntity = $this->_em->find($queryClass, $entity->id);
self::assertSame('root', $loadedEntity->root);
self::assertSame('middle', $loadedEntity->middle);
self::assertSame('leaf', $loadedEntity->leaf);
}
public static function queryClasses(): array
{
return [
'query via root entity' => [GH8127Root::class],
'query via leaf entity' => [GH8127Leaf::class],
];
}
}
/**
* @ORM\Entity
* @ORM\Table(name="root")
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorMap({ "leaf": "GH8127Leaf" })
*/
abstract class GH8127Root
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\Column
*
* @var string
*/
public $root;
}
/**
* @ORM\Entity
*/
abstract class GH8127Middle extends GH8127Root
{
/**
* @ORM\Column
*
* @var string
*/
public $middle;
}
/**
* @ORM\Entity
*/
class GH8127Leaf extends GH8127Middle
{
/**
* @ORM\Column
*
* @var string
*/
public $leaf;
}
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH8415Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema(
[
GH8415BaseClass::class,
GH8415MiddleMappedSuperclass::class,
GH8415LeafClass::class,
GH8415AssociationTarget::class,
]
);
}
public function testAssociationIsBasedOnBaseClass(): void
{
$target = new GH8415AssociationTarget();
$leaf = new GH8415LeafClass();
$leaf->baseField = 'base';
$leaf->middleField = 'middle';
$leaf->leafField = 'leaf';
$leaf->target = $target;
$this->_em->persist($target);
$this->_em->persist($leaf);
$this->_em->flush();
$this->_em->clear();
$query = $this->_em->createQuery('SELECT leaf FROM Doctrine\Tests\ORM\Functional\Ticket\GH8415LeafClass leaf JOIN leaf.target t');
$result = $query->getOneOrNullResult();
$this->assertInstanceOf(GH8415LeafClass::class, $result);
$this->assertSame('base', $result->baseField);
$this->assertSame('middle', $result->middleField);
$this->assertSame('leaf', $result->leafField);
}
}
/**
* @ORM\Entity
*/
class GH8415AssociationTarget
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
}
/**
* @ORM\Entity
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="discriminator", type="string")
* @ORM\DiscriminatorMap({"1" = "Doctrine\Tests\ORM\Functional\Ticket\GH8415BaseClass", "2" = "Doctrine\Tests\ORM\Functional\Ticket\GH8415LeafClass"})
*/
class GH8415BaseClass
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity="GH8415AssociationTarget")
*
* @var GH8415AssociationTarget
*/
public $target;
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $baseField;
}
/**
* @ORM\MappedSuperclass
*/
class GH8415MiddleMappedSuperclass extends GH8415BaseClass
{
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $middleField;
}
/**
* @ORM\Entity
*/
class GH8415LeafClass extends GH8415MiddleMappedSuperclass
{
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $leafField;
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmTestCase;
class GH8415ToManyAssociationTest extends OrmTestCase
{
public function testToManyAssociationOnBaseClassAllowedWhenThereAreMappedSuperclassesAsChildren(): void
{
$this->expectNotToPerformAssertions();
$em = $this->getTestEntityManager();
$em->getClassMetadata(GH8415ToManyLeafClass::class);
}
}
/**
* @ORM\Entity
*/
class GH8415ToManyAssociationTarget
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity="GH8415ToManyBaseClass", inversedBy="targets")
*
* @var GH8415ToManyBaseClass
*/
public $base;
}
/**
* @ORM\Entity
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="discriminator", type="string")
* @ORM\DiscriminatorMap({"1" = "Doctrine\Tests\ORM\Functional\Ticket\GH8415ToManyBaseClass", "2" = "Doctrine\Tests\ORM\Functional\Ticket\GH8415ToManyLeafClass"})
*/
class GH8415ToManyBaseClass
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(targetEntity="GH8415ToManyAssociationTarget", mappedBy="base")
*
* @var Collection
*/
public $targets;
}
/**
* @ORM\MappedSuperclass
*/
class GH8415ToManyMappedSuperclass extends GH8415ToManyBaseClass
{
}
/**
* @ORM\Entity
*/
class GH8415ToManyLeafClass extends GH8415ToManyMappedSuperclass
{
/**
* @ORM\Column(type="string")
*
* @var string
*/
public $leafField;
}
@@ -37,7 +37,7 @@ class GH9230Test extends OrmFunctionalTestCase
/**
* This does not work before the fix in PR#9663, but is does work after the fix is applied
*/
public function failingValuesBeforeFix(): array
public static function failingValuesBeforeFix(): array
{
return [
'string=""' => ['name', '', 'test name'],
@@ -66,7 +66,7 @@ class GH9230Test extends OrmFunctionalTestCase
/**
* This already works before the fix in PR#9663 is applied because none of these are falsy values in php
*/
public function succeedingValuesBeforeFix(): array
public static function succeedingValuesBeforeFix(): array
{
return [
'string="test"' => ['name', 'test', 'test2'],
+77
View File
@@ -0,0 +1,77 @@
<?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\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\MappedSuperclass;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH9516Test extends OrmFunctionalTestCase
{
public function testEntityCanHaveInverseOneToManyAssociationWithChildMappedSuperclass(): void
{
$sportsCarMetadata = $this->_em->getClassMetadata(GH9516SportsCar::class);
$this->assertTrue($sportsCarMetadata->hasAssociation('passengers'));
}
}
/** @Entity */
class GH9516Passenger
{
/**
* @Id
* @Column(type="integer")
* @var int $id
*/
private $id;
/**
* @ManyToOne(targetEntity="GH9516Vehicle", inversedBy="passengers")
* @var GH9516Vehicle $vehicle
*/
private $vehicle;
}
/**
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({ "sports" = "\Doctrine\Tests\ORM\Functional\Ticket\GH9516SportsCar" })
* @ORM\InheritanceType("SINGLE_TABLE")
*
* @Entity
*/
abstract class GH9516Vehicle
{
/**
* @Id
* @Column(type="integer")
* @var int $id
*/
private $id;
/**
* @OneToMany(targetEntity="GH9516Passenger", mappedBy="vehicle")
* @var GH9516Passenger[] $passengers
*/
private $passengers;
}
/**
* @MappedSuperclass
*/
abstract class GH9516Car extends GH9516Vehicle
{
}
/**
* @Entity
*/
class GH9516SportsCar extends GH9516Car
{
}
@@ -345,7 +345,7 @@ class ValueObjectsTest extends OrmFunctionalTestCase
}
/** @psalm-return list<array{string, string}> */
public function getInfiniteEmbeddableNestingData(): array
public static function getInfiniteEmbeddableNestingData(): array
{
return [
['DDCInfiniteNestingEmbeddable', 'DDCInfiniteNestingEmbeddable'],
@@ -17,7 +17,7 @@ use Doctrine\Tests\Models\Forum\ForumCategory;
class ArrayHydratorTest extends HydrationTestCase
{
/** @psalm-return list<array{mixed}> */
public function provideDataForUserEntityResult(): array
public static function provideDataForUserEntityResult(): array
{
return [
[0],
@@ -29,13 +29,14 @@ use Doctrine\Tests\Models\Hydration\SimpleEntity;
use Doctrine\Tests\PHPUnitCompatibility\MockBuilderCompatibilityTools;
use function count;
use function property_exists;
class ObjectHydratorTest extends HydrationTestCase
{
use MockBuilderCompatibilityTools;
/** @psalm-return list<array{mixed}> */
public function provideDataForUserEntityResult(): array
public static function provideDataForUserEntityResult(): array
{
return [
[0],
@@ -44,7 +45,7 @@ class ObjectHydratorTest extends HydrationTestCase
}
/** @psalm-return list<array{mixed, mixed}> */
public function provideDataForMultipleRootEntityResult(): array
public static function provideDataForMultipleRootEntityResult(): array
{
return [
[0, 0],
@@ -55,7 +56,7 @@ class ObjectHydratorTest extends HydrationTestCase
}
/** @psalm-return list<array{mixed}> */
public function provideDataForProductEntityResult(): array
public static function provideDataForProductEntityResult(): array
{
return [
[0],
@@ -927,10 +928,10 @@ class ObjectHydratorTest extends HydrationTestCase
self::assertEquals(1, $result[0]->getId());
self::assertEquals(2, $result[1]->getId());
self::assertObjectHasAttribute('boards', $result[0]);
self::assertTrue(property_exists($result[0], 'boards'));
self::assertEquals(3, count($result[0]->boards));
self::assertObjectHasAttribute('boards', $result[1]);
self::assertTrue(property_exists($result[1], 'boards'));
self::assertEquals(1, count($result[1]->boards));
}
@@ -37,7 +37,7 @@ class AssignedGeneratorTest extends OrmTestCase
$this->assignedGen->generateId($this->entityManager, $entity);
}
public function entitiesWithoutId(): array
public static function entitiesWithoutId(): array
{
return [
'single' => [new AssignedSingleIdEntity()],
@@ -154,7 +154,7 @@ class HydrationCompleteHandlerTest extends TestCase
}
/** @psalm-return list<array{int}> */
public function invocationFlagProvider(): array
public static function invocationFlagProvider(): array
{
return [
[ListenersInvoker::INVOKE_LISTENERS],
@@ -276,7 +276,7 @@ class AnnotationDriverTest extends MappingDriverTestCase
self::assertSame($expectedLength, $metadata->discriminatorColumn['length']);
}
public function provideDiscriminatorColumnTestcases(): Generator
public static function provideDiscriminatorColumnTestcases(): Generator
{
yield [DiscriminatorColumnWithNullLength::class, 255];
yield [DiscriminatorColumnWithNoLength::class, 255];
@@ -181,16 +181,10 @@ class ClassMetadataFactoryTest extends OrmTestCase
$driver = $this->createMock(MappingDriver::class);
$driver->expects(self::exactly(2))
->method('isTransient')
->withConsecutive(
[CmsUser::class],
[CmsArticle::class]
)
->willReturnMap(
[
[CmsUser::class, true],
[CmsArticle::class, false],
]
);
->willReturnMap([
[CmsUser::class, true],
[CmsArticle::class, false],
]);
$em = $this->createEntityManager($driver);
@@ -69,49 +69,47 @@ class ReflectionEmbeddedPropertyTest extends TestCase
}
/**
* Data provider
*
* @return ReflectionProperty[][]|string[][]
*/
public function getTestedReflectionProperties(): array
public static function getTestedReflectionProperties(): array
{
return [
[
$this->getReflectionProperty(BooleanModel::class, 'id'),
$this->getReflectionProperty(BooleanModel::class, 'id'),
self::getReflectionProperty(BooleanModel::class, 'id'),
self::getReflectionProperty(BooleanModel::class, 'id'),
BooleanModel::class,
],
// reflection on embeddables that have properties defined in abstract ancestors:
[
$this->getReflectionProperty(BooleanModel::class, 'id'),
$this->getReflectionProperty(AbstractEmbeddable::class, 'propertyInAbstractClass'),
self::getReflectionProperty(BooleanModel::class, 'id'),
self::getReflectionProperty(AbstractEmbeddable::class, 'propertyInAbstractClass'),
ConcreteEmbeddable::class,
],
[
$this->getReflectionProperty(BooleanModel::class, 'id'),
$this->getReflectionProperty(ConcreteEmbeddable::class, 'propertyInConcreteClass'),
self::getReflectionProperty(BooleanModel::class, 'id'),
self::getReflectionProperty(ConcreteEmbeddable::class, 'propertyInConcreteClass'),
ConcreteEmbeddable::class,
],
// reflection on classes extending internal PHP classes:
[
$this->getReflectionProperty(ArrayObjectExtendingClass::class, 'publicProperty'),
$this->getReflectionProperty(ArrayObjectExtendingClass::class, 'privateProperty'),
self::getReflectionProperty(ArrayObjectExtendingClass::class, 'publicProperty'),
self::getReflectionProperty(ArrayObjectExtendingClass::class, 'privateProperty'),
ArrayObjectExtendingClass::class,
],
[
$this->getReflectionProperty(ArrayObjectExtendingClass::class, 'publicProperty'),
$this->getReflectionProperty(ArrayObjectExtendingClass::class, 'protectedProperty'),
self::getReflectionProperty(ArrayObjectExtendingClass::class, 'publicProperty'),
self::getReflectionProperty(ArrayObjectExtendingClass::class, 'protectedProperty'),
ArrayObjectExtendingClass::class,
],
[
$this->getReflectionProperty(ArrayObjectExtendingClass::class, 'publicProperty'),
$this->getReflectionProperty(ArrayObjectExtendingClass::class, 'publicProperty'),
self::getReflectionProperty(ArrayObjectExtendingClass::class, 'publicProperty'),
self::getReflectionProperty(ArrayObjectExtendingClass::class, 'publicProperty'),
ArrayObjectExtendingClass::class,
],
];
}
private function getReflectionProperty(string $className, string $propertyName): ReflectionProperty
private static function getReflectionProperty(string $className, string $propertyName): ReflectionProperty
{
$reflectionProperty = new ReflectionProperty($className, $propertyName);
@@ -22,6 +22,10 @@ use Doctrine\Tests\Models\DDC889\DDC889SuperClass;
use Doctrine\Tests\Models\Generic\BooleanModel;
use Doctrine\Tests\Models\GH7141\GH7141Article;
use Doctrine\Tests\Models\GH7316\GH7316Article;
use Doctrine\Tests\Models\Project\Project;
use Doctrine\Tests\Models\Project\ProjectId;
use Doctrine\Tests\Models\Project\ProjectInvalidMapping;
use Doctrine\Tests\Models\Project\ProjectName;
use Doctrine\Tests\Models\ValueObjects\Name;
use Doctrine\Tests\Models\ValueObjects\Person;
@@ -239,6 +243,10 @@ class XmlMappingDriverTest extends MappingDriverTestCase
UserMissingAttributes::class,
['The attribute \'name\' is required but missing' => 1],
],
[
ProjectInvalidMapping::class,
['attribute \'type\': [facet \'pattern\'] The value' => 2],
],
];
}
@@ -279,6 +287,23 @@ class XmlMappingDriverTest extends MappingDriverTestCase
$this->createClassMetadata(DDC889Class::class);
}
public function testClassNameInFieldOrId(): void
{
$class = new ClassMetadata(Project::class);
$class->initializeReflection(new RuntimeReflectionService());
$driver = $this->loadDriver();
$driver->loadMetadataForClass(Project::class, $class);
/** @var array{type: string} $id */
$id = $class->getFieldMapping('id');
/** @var array{type: string} $name */
$name = $class->getFieldMapping('name');
self::assertEquals(ProjectId::class, $id['type']);
self::assertEquals(ProjectName::class, $name['type']);
}
}
class CTI
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Doctrine\Tests\Models\Project\Project" table="project">
<id name="id" type="Doctrine\Tests\Models\Project\ProjectId" column="id">
<generator strategy="NONE"/>
</id>
<field name="name" type="Doctrine\Tests\Models\Project\ProjectName" column="name"/>
</entity>
</doctrine-mapping>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Doctrine\Tests\Models\Project\ProjectInvalidMapping" table="project">
<id name="id" type="Doctrine/Tests/Models/Project/Project/ProjectId" column="id">
<generator strategy="NONE"/>
</id>
<field name="name" type="Doctrine/Tests/Models/Project/Project/ProjectName" column="name"/>
</entity>
</doctrine-mapping>
@@ -7,10 +7,8 @@ namespace Doctrine\Tests\ORM;
use Doctrine\ORM\ORMInvalidArgumentException;
use PHPUnit\Framework\TestCase;
use stdClass;
use Stringable;
use function spl_object_id;
use function uniqid;
/** @covers \Doctrine\ORM\ORMInvalidArgumentException */
class ORMInvalidArgumentExceptionTest extends TestCase
@@ -29,7 +27,7 @@ class ORMInvalidArgumentExceptionTest extends TestCase
}
/** @psalm-return list<array{mixed, string}> */
public function invalidEntityNames(): array
public static function invalidEntityNames(): array
{
return [
[null, 'Entity name must be a string, null given'],
@@ -49,30 +47,32 @@ class ORMInvalidArgumentExceptionTest extends TestCase
self::assertSame($expectedMessage, $exception->getMessage());
}
public function newEntitiesFoundThroughRelationshipsErrorMessages(): array
public static function newEntitiesFoundThroughRelationshipsErrorMessages(): array
{
$stringEntity3 = uniqid('entity3', true);
$entity1 = new stdClass();
$entity2 = new stdClass();
$entity3 = $this->createMock(Stringable::class);
$association1 = [
$entity1 = new stdClass();
$entity2 = new stdClass();
$entity3 = new class {
public function __toString(): string
{
return 'ThisIsAStringRepresentationOfEntity3';
}
};
$association1 = [
'sourceEntity' => 'foo1',
'fieldName' => 'bar1',
'targetEntity' => 'baz1',
];
$association2 = [
$association2 = [
'sourceEntity' => 'foo2',
'fieldName' => 'bar2',
'targetEntity' => 'baz2',
];
$association3 = [
$association3 = [
'sourceEntity' => 'foo3',
'fieldName' => 'bar3',
'targetEntity' => 'baz3',
];
$entity3->method('__toString')->willReturn($stringEntity3);
return [
'one entity found' => [
[
@@ -121,7 +121,7 @@ class ORMInvalidArgumentExceptionTest extends TestCase
],
],
'A new entity was found through the relationship \'foo3#bar3\' that was not configured to cascade '
. 'persist operations for entity: ' . $stringEntity3
. 'persist operations for entity: ThisIsAStringRepresentationOfEntity3'
. '. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity '
. 'or configure cascade persist this association in the mapping for example '
. '@ManyToOne(..,cascade={"persist"}).',
+2 -2
View File
@@ -278,7 +278,7 @@ class ExprTest extends OrmTestCase
self::assertEquals(':groupId MEMBER OF u.groups', (string) $this->expr->isMemberOf(':groupId', 'u.groups'));
}
public function provideIterableValue(): Generator
public static function provideIterableValue(): Generator
{
$gen = static function () {
yield from [1, 2, 3];
@@ -288,7 +288,7 @@ class ExprTest extends OrmTestCase
yield 'generator' => [$gen()];
}
public function provideLiteralIterableValue(): Generator
public static function provideLiteralIterableValue(): Generator
{
$gen = static function () {
yield from ['foo', 'bar'];
@@ -88,7 +88,7 @@ class LanguageRecognitionTest extends OrmTestCase
}
/** @psalm-return list<array{string}> */
public function invalidDQL(): array
public static function invalidDQL(): array
{
return [
@@ -573,12 +573,11 @@ class LanguageRecognitionTest extends OrmTestCase
$this->assertValidDQL('SELECT g FROM ' . __NAMESPACE__ . '\DQLKeywordsModelGroup g WHERE g.from=0');
}
/* The exception is currently thrown in the SQLWalker, not earlier.
public function testInverseSideSingleValuedAssociationPathNotAllowed()
public function testInverseSideSingleValuedAssociationPathNotAllowed(): void
{
self::markTestSkipped('The exception is currently thrown in the SQLWalker, not earlier.');
$this->assertInvalidDQL('SELECT u.id FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.address = ?1');
}
*/
/** @group DDC-617 */
public function testSelectOnlyNonRootEntityAlias(): void
+1 -1
View File
@@ -226,7 +226,7 @@ class LexerTest extends OrmTestCase
}
/** @psalm-return list<array{int, string}> */
public function provideTokens(): array
public static function provideTokens(): array
{
return [
[Lexer::T_IDENTIFIER, 'u'], // one char
@@ -21,7 +21,7 @@ use const PHP_VERSION_ID;
class ParameterTypeInfererTest extends OrmTestCase
{
/** @psalm-return Generator<string, array{mixed, (int|string)}> */
public function providerParameterTypeInferer(): Generator
public static function providerParameterTypeInferer(): Generator
{
yield 'integer' => [1, Types::INTEGER];
yield 'string' => ['bar', ParameterType::STRING];
+13 -2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Query;
use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
@@ -12,6 +13,8 @@ use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmTestCase;
use stdClass;
use function class_exists;
class ParserTest extends OrmTestCase
{
/**
@@ -53,6 +56,10 @@ class ParserTest extends OrmTestCase
*/
public function testAbstractSchemaNameSupportsNamespaceAlias(): void
{
if (! class_exists(PersistentObject::class)) {
self::markTestSkipped('This test requires doctrine/persistence 2');
}
$parser = $this->createParser('CMS:CmsUser');
$parser->getEntityManager()->getConfiguration()->addEntityNamespace('CMS', 'Doctrine\Tests\Models\CMS');
@@ -66,6 +73,10 @@ class ParserTest extends OrmTestCase
*/
public function testAbstractSchemaNameSupportsNamespaceAliasWithRelativeClassname(): void
{
if (! class_exists(PersistentObject::class)) {
self::markTestSkipped('This test requires doctrine/persistence 2');
}
$parser = $this->createParser('Model:CMS\CmsUser');
$parser->getEntityManager()->getConfiguration()->addEntityNamespace('Model', 'Doctrine\Tests\Models');
@@ -102,7 +113,7 @@ class ParserTest extends OrmTestCase
}
/** @psalm-return list<array{int, string}> */
public function validMatches()
public static function validMatches(): array
{
/*
* This only covers the special case handling in the Parser that some
@@ -123,7 +134,7 @@ class ParserTest extends OrmTestCase
}
/** @psalm-return list<array{int, string}> */
public function invalidMatches(): array
public static function invalidMatches(): array
{
return [
[Lexer::T_DOT, 'ALL'], // ALL is a terminal string (reserved keyword) and also possibly an identifier

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