Compare commits

...

63 Commits

Author SHA1 Message Date
Grégoire Paris
c1bb2ccf4b Merge pull request #11526 from GromNaN/patch-1
doc: Use modern array syntax in getting started
2024-06-26 19:24:40 +02:00
Jérôme Tamarelle
e3d7c6076c Use modern array syntax in the doc 2024-06-26 19:18:32 +02:00
Grégoire Paris
40f299f1eb Merge pull request #11506 from michalbundyra/composite-key-relations-3
[2.19.x] Fetching entities with Composite Key Relations and null values
2024-06-21 08:12:27 +02:00
Grégoire Paris
68af854f46 Merge pull request #11513 from greg0ire/address-persistence-3.3.3-release
Address doctrine/persistence 3.3.3 release
2024-06-20 22:14:52 +02:00
Grégoire Paris
77467cd824 Address doctrine/persistence 3.3.3 release
FileDriver became templatable, and some very wrong phpdoc has been
fixed, causing Psalm to better understand the 2 FileDriver classes in
this project.
2024-06-20 22:00:33 +02:00
Grégoire Paris
802f20b8e7 Merge pull request #11509 from greg0ire/remove-unneeded-rule
Remove unneeded CS rule
2024-06-19 23:49:15 +02:00
Michał Bundyra
96d13ac62a Fetching entities with Composite Key Relations and null values
Remove redundant condition to check if target class contains foreign
identifier in order to allow fetching a null for relations with
composite keys, when part of the key value is null.
2024-06-19 21:54:02 +01:00
Grégoire Paris
2ea6a1a5fb Remove unneeded CS rule 2024-06-19 21:47:55 +02:00
Grégoire Paris
cc2ad1993c Merge pull request #11501 from gitbugr/2.19.x
Fix OneToManyPersister::deleteEntityCollection missing discriminator column/value. (GH-11500)
2024-06-17 21:40:07 +02:00
Kyron Taylor
e4d46c4276 Fix OneToManyPersister::deleteEntityCollection missing discriminator column/value. (GH-11500) 2024-06-15 21:58:08 +01:00
Grégoire Paris
858a1adc3b Merge pull request #11194 from noemi-salaun/fix/gh10889
Skip joined entity creation for empty relation (#10889)
2024-06-14 20:06:59 +02:00
Noemi Salaun
3b499132d9 Skip joined entity creation for empty relation (#10889) 2024-06-14 14:34:04 +02:00
Daniel Black
39153fd88a ci: maintained and stable mariadb version (11.4 current lts) (#11490)
Also use MARIADB env names and the healthcheck.sh included in the container.
2024-06-13 19:34:46 +02:00
Grégoire Paris
bdc9679e37 Merge pull request #11493 from SamMousa/fix-docs-11492
fix(docs): use string value in `addAttribute`
2024-06-11 15:26:45 +01:00
Sam Mousa
87a8ee21c9 fix(docs): use string value in addAttribute 2024-06-11 16:21:28 +02:00
Grégoire Paris
59c8bc09ab Replace assertion with exception (#11489) 2024-06-03 23:08:27 +02:00
Grégoire Paris
3a7d7c9f57 Merge pull request #11484 from greg0ire/backport-ramsey
Use ramsey/composer-install in PHPBench workflow
2024-06-02 15:26:00 +02:00
Grégoire Paris
06eca40134 Use ramsey/composer-install in PHPBench workflow
It will handle caching for us.
2024-06-02 15:22:59 +02:00
Grégoire Paris
23b35e9554 Merge pull request #11475 from nicolas-grekas/fix-clone
Fix cloning entities
2024-06-01 22:47:57 +02:00
Grégoire Paris
e063926cbd Merge pull request #11445 from aprat84/gh-11128
Consider usage of setFetchMode when checking for simultaneous usage of fetch-mode EAGER and WITH condition
2024-05-30 17:24:11 +02:00
Grégoire Paris
4a01a76a17 Merge pull request #11460 from IndraGunawan/update-transactional-doc
docs: update EntityManager#transactional to EntityManager#wrapInTransaction
2024-05-28 14:07:06 +02:00
Indra Gunawan
93c2dd9d4b update EntityManager#transactional to EntityManager#wrapInTransaction
One has been deprecated in favor of the other.
2024-05-28 13:59:17 +02:00
Nicolas Grekas
75bc22980e Fix cloning entities 2024-05-27 14:53:58 +02:00
Alix Mauro
9696c3434d Consider usage of setFetchMode when checking for simultaneous usage of fetch-mode EAGER and WITH condition.
This fixes a bug that arises when an entity relation is mapped with
fetch-mode EAGER but setFetchMode LAZY (or anything that is not EAGER)
has been used on the query. If the query use WITH condition, an
exception is incorrectly raised (Associations with fetch-mode=EAGER may
not be using WITH conditions).

Fixes #11128

Co-Authored-By: Albert Prat <albert.prat@interactiu.cat>
2024-05-25 14:22:20 +02:00
Alexander M. Turek
d31aabb40c Psalm 5.24.0 (#11467) 2024-05-21 14:21:50 +02:00
Alexander M. Turek
d66884403f PHPStan 1.11.1 (#11466) 2024-05-21 13:32:25 +02:00
Alexander M. Turek
552eae37a3 Test with actual lock modes (#11465) 2024-05-21 12:30:36 +02:00
Alexander M. Turek
ee4b03aa78 Backport test for Query::setLockMode() (#11463) 2024-05-21 12:30:16 +02:00
dependabot[bot]
c5291b4de8 Bump ramsey/composer-install from 2 to 3 (#11442)
Bumps [ramsey/composer-install](https://github.com/ramsey/composer-install) from 2 to 3.
- [Release notes](https://github.com/ramsey/composer-install/releases)
- [Commits](https://github.com/ramsey/composer-install/compare/v2...v3)

---
updated-dependencies:
- dependency-name: ramsey/composer-install
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-05 23:47:43 +02:00
Grégoire Paris
831d86548c Merge pull request #11441 from doctrine/dependabot/github_actions/2.19.x/doctrine/dot-github-5.0.1
Bump doctrine/.github from 3.0.0 to 5.0.1
2024-05-05 23:23:39 +02:00
dependabot[bot]
f26b3b9cf9 Bump doctrine/.github from 3.0.0 to 5.0.1
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 3.0.0 to 5.0.1.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/3.0.0...5.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-05 21:17:24 +00:00
Grégoire Paris
9ab84f7478 Merge pull request #11440 from greg0ire/update-codecov
Upgrade codecov/codecov-action
2024-05-05 22:56:55 +02:00
Grégoire Paris
e6bb4ef20e Upgrade codecov/codecov-action 2024-05-05 22:43:51 +02:00
Grégoire Paris
94986af284 Merge pull request #11430 from W0rma/fix-deprecation-layer-orm-exception
Fix deprecation layer of Doctrine\ORM\ORMException
2024-04-30 08:49:54 +02:00
W0rma
ad5c8e4bdc Make test compatible with PHP 7.1 2024-04-30 08:35:06 +02:00
W0rma
c363f55ad1 Fix deprecation layer 2024-04-29 14:48:36 +02:00
Grégoire Paris
c973a62272 Merge pull request #11429 from SenseException/unused-test-group
Remove unused test group
2024-04-27 11:42:05 +02:00
Grégoire Paris
8d3446015a Merge pull request #11428 from xificurk/keep-removed-entity-in-identity-map
Prevent creation of new MANAGED entity instance by reloading REMOVED entity from database
2024-04-27 11:40:56 +02:00
Claudio Zizza
4e335f4044 Remove unused test group 2024-04-27 10:46:19 +02:00
Petr Morávek
bb36d49b38 Keep entities in identity map until the scheduled deletions are executed.
If the entity gets reloaded from database before the deletions are
executed UnitOfWork needs to be able to return the original instance in
REMOVED state.
2024-04-26 21:54:02 +02:00
Grégoire Paris
2b81a8e260 Merge pull request #11426 from nasimic/patch-1
Update association-mapping.rst
2024-04-26 21:27:07 +02:00
Nasimi Mammadov
7d3b3f28e9 Update association-mapping.rst
Changed capitalized column names to lowercase for consistency. Other occurances of column names mentioned as lowercase several times at this same page.
2024-04-26 21:24:28 +02:00
Simon Podlipsky
cbec236e8b fix: always cleanup in AbstractHydrator::toIterable() (#11101)
Previously it didn't cleanup anything as long as the iteration hasn't reached the final row.

Co-authored-by: Oleg Andreyev <oleg.andreyev@lampa.lv>
2024-04-25 10:32:40 +02:00
Grégoire Paris
306963fe79 Merge pull request #11422 from tomasz-ryba/bugfix/fetch-eager-order-by
Bugfix: respect orderBy for fetch EAGER mode
2024-04-25 00:09:43 +02:00
Tomasz Ryba
fb4578406f Respect orderBy for EAGER fetch mode
EAGER fetch mode ignores orderBy as of changes introduced with #8391

Fixes #11163
Fixes #11381
2024-04-24 22:44:16 +02:00
Grégoire Paris
bdc41e2b5e Merge pull request #11420 from tyteen4a03/patch-1
fix(docs): typo
2024-04-22 15:40:39 +02:00
Timothy Choi
90376a6431 fix(docs): typo 2024-04-22 15:30:56 +02:00
Alexander M. Turek
b274893486 Fix BIGINT validation (#11414) 2024-04-15 15:11:10 +02:00
Vincent Langlet
8709fb38b0 Fix templated phpdoc return type (#11407)
* Improve getClassMetadata phpdoc

* Update baseline
2024-04-01 12:44:58 +02:00
Grégoire Paris
e9e60f2fbc Merge pull request #11403 from ThomasLandauer/patch-10
[Documentation] Merging "Query Result Formats" with "Hydration Modes"
2024-03-28 07:51:53 +01:00
Thomas Landauer
5f3c1dbab8 [Documentation] Merging "Query Result Formats" with "Hydration Modes"
Page: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/dql-doctrine-query-language.html#query-result-formats

As announced in https://github.com/doctrine/orm/pull/11372#issue-2190613801, I merged the (mostly) identical sections.

* I changed the `const`s from `Query` to `AbstractQuery`
* I deleted this - mainly cause I didn't find a nice place for it:
    > In parentheses are the constants of the ``Query`` class which you can use with the
general-purpose method ``Query::execute(array $params = [], $hydrationMode = Query::HYDRATE_OBJECT)``.
2024-03-27 13:52:50 +01:00
Grégoire Paris
6090141e0b Merge pull request #11389 from tantegerda1/2.19.x
Fix psalm errors: remove override of template type
2024-03-23 11:13:39 +01:00
Grégoire Paris
e4a6c041b5 Merge pull request #11372 from ThomasLandauer/patch-12
[Documentation] Query Result Formats
2024-03-23 10:07:42 +01:00
Ludwig Rafelsberger
c54c557e02 Fix psalm errors: remove override of template type
See https://github.com/doctrine/collections/issues/368 for the same
issue in doctrine/collections which has been fixed there.

The issue happens when using ->contains(). Running psalm emits

  > InvalidArgument - Argument 1 of Doctrine\ORM\PersistentCollection::contains
  > expects
  > TMaybeContained:fn-doctrine\common\collections\readablecollection::contains
  > as mixed, but … provided.

Solution: we should either not define @template TMaybeContained or
re-define the complete psalm docblock from ReadableCollection.

Repairing the docblock necessitates an update to the psalm baseline:
one "known issue" is no longer an issue and thus removed.
2024-03-22 11:05:00 +01:00
Thomas Landauer
46d0865339 Update dql-doctrine-query-language.rst 2024-03-21 17:55:39 +01:00
Alexander M. Turek
1a5a4c674a Set column length explicitly (#11393) 2024-03-21 12:01:42 +01:00
Alexander M. Turek
95795c87a8 Add missing import 2024-03-21 10:38:59 +01:00
Alexander M. Turek
db6e702088 Remove unused variable (#11391) 2024-03-21 10:32:55 +01:00
Thomas Landauer
5ccbc201bf [Documentation] Removing "Doctrine Mapping Types" ... (#11384)
... in favor of https://www.doctrine-project.org/projects/doctrine-dbal/en/3.8/reference/types.html#reference

Page: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/basic-mapping.html#doctrine-mapping-types

As announced in https://github.com/doctrine/dbal/pull/6336#issuecomment-2003720361 , the goal is to remove this duplicated type information from ORM and replace it with a link to DBAL.

In https://github.com/doctrine/dbal/pull/6341 , I'm adding any detail which I'm deleting here to the DBAL.
2024-03-20 23:34:10 +01:00
Benjamin Eberlei
d15624f72f [GH-11185] Bugfix: do not use collection batch loading for indexBy assocations. (#11380) 2024-03-20 15:45:47 +01:00
Benjamin Eberlei
9d1a4973ae Improve lazy ghost performance by avoiding self-referencing closure. (#11376)
* Improve lazy ghost performance by avoiding self-referencing closure.

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

* update baselien

---------

Co-authored-by: Nicolas Grekas <nicolas.grekas@gmail.com>
2024-03-19 09:19:25 +01:00
Thomas Landauer
4b4b9b7b6f Adding NonUniqueResultException 2024-03-17 12:25:05 +01:00
Thomas Landauer
ae842259f5 [Documentation] Query Result Formats
Page: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/dql-doctrine-query-language.html#query-result-formats

Follow-up of https://github.com/doctrine/orm/pull/11359

The table I suggested is probably not working, since the text for each method is too long. And what I really wanted is to make it more *scanable*. So I tried boldfacing - if this doesn't work, I'll try something else.

Questions:

1. This section here is basically the same as https://www.doctrine-project.org/projects/doctrine-orm/en/2.10/reference/dql-doctrine-query-language.html#hydration-modes ! So I'll try to merge them (in another PR), OK? I think the list is a better format (more scanable) - since those methods all work the same, there's no need for a full-blown code sample for each, IMO.

2. `getSingleColumnResult()` is missing.
2024-03-17 12:24:10 +01:00
59 changed files with 1369 additions and 494 deletions

View File

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

View File

@@ -80,7 +80,7 @@ jobs:
if: "${{ matrix.dbal-version != 'default' }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"
@@ -162,7 +162,7 @@ jobs:
if: "${{ matrix.dbal-version != 'default' }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"
@@ -190,7 +190,7 @@ jobs:
- "default"
- "3@dev"
mariadb-version:
- "10.9"
- "11.4"
extension:
- "mysqli"
- "pdo_mysql"
@@ -204,11 +204,11 @@ jobs:
mariadb:
image: "mariadb:${{ matrix.mariadb-version }}"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: "doctrine_tests"
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes
MARIADB_DATABASE: "doctrine_tests"
options: >-
--health-cmd "mysqladmin ping --silent"
--health-cmd "healthcheck.sh --connect --innodb_initialized"
ports:
- "3306:3306"
@@ -232,7 +232,7 @@ jobs:
extensions: "${{ matrix.extension }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"
@@ -302,7 +302,7 @@ jobs:
if: "${{ matrix.dbal-version != 'default' }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"
@@ -348,7 +348,7 @@ jobs:
ini-values: "zend.assertions=1, apc.enable_cli=1"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "${{ matrix.deps }}"
@@ -377,6 +377,8 @@ jobs:
path: "reports"
- name: "Upload to Codecov"
uses: "codecov/codecov-action@v3"
uses: "codecov/codecov-action@v4"
with:
directory: reports
env:
CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}"

View File

@@ -36,7 +36,7 @@ jobs:
run: "composer require --dev phpdocumentor/guides-cli --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "highest"

View File

@@ -47,15 +47,8 @@ jobs:
coverage: "pcov"
ini-values: "zend.assertions=1, apc.enable_cli=1"
- name: "Cache dependencies installed with composer"
uses: "actions/cache@v3"
with:
path: "~/.composer/cache"
key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}"
restore-keys: "php-${{ matrix.php-version }}-composer-locked-"
- name: "Install dependencies with composer"
run: "composer update --no-interaction --no-progress"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
- name: "Run PHPBench"
run: "vendor/bin/phpbench run --report=default"

View File

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

View File

@@ -58,7 +58,7 @@ jobs:
run: "composer require doctrine/persistence ^$([ ${{ matrix.persistence-version }} = default ] && echo '3.1' || echo ${{ matrix.persistence-version }}) --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "highest"
@@ -95,7 +95,7 @@ jobs:
run: "composer require doctrine/persistence ^3.1 --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "highest"

View File

@@ -42,14 +42,14 @@
"doctrine/annotations": "^1.13 || ^2",
"doctrine/coding-standard": "^9.0.2 || ^12.0",
"phpbench/phpbench": "^0.16.10 || ^1.0",
"phpstan/phpstan": "~1.4.10 || 1.10.59",
"phpstan/phpstan": "~1.4.10 || 1.11.1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
"vimeo/psalm": "4.30.0 || 5.22.2"
"vimeo/psalm": "4.30.0 || 5.24.0"
},
"conflict": {
"doctrine/annotations": "<1.13 || >= 3.0"

View File

@@ -11,7 +11,7 @@ What we offer are hooks to execute any kind of validation.
.. note::
You don't need to validate your entities in the lifecycle
events. Its only one of many options. Of course you can also
events. It is only one of many options. Of course you can also
perform validations in value setters or any other method of your
entities that are used in your code.

View File

@@ -1300,8 +1300,8 @@ This is essentially the same as the following, more verbose, mapping:
* @var Collection<int, Group>
*/
#[JoinTable(name: 'User_Group')]
#[JoinColumn(name: 'User_id', referencedColumnName: 'id')]
#[InverseJoinColumn(name: 'Group_id', referencedColumnName: 'id')]
#[JoinColumn(name: 'user_id', referencedColumnName: 'id')]
#[InverseJoinColumn(name: 'group_id', referencedColumnName: 'id')]
#[ManyToMany(targetEntity: Group::class)]
private Collection $groups;
// ...
@@ -1333,10 +1333,10 @@ This is essentially the same as the following, more verbose, mapping:
<many-to-many field="groups" target-entity="Group">
<join-table name="User_Group">
<join-columns>
<join-column id="User_id" referenced-column-name="id" />
<join-column id="user_id" referenced-column-name="id" />
</join-columns>
<inverse-join-columns>
<join-column id="Group_id" referenced-column-name="id" />
<join-column id="group_id" referenced-column-name="id" />
</inverse-join-columns>
</join-table>
</many-to-many>

View File

@@ -300,50 +300,12 @@ and a custom ``Doctrine\ORM\Mapping\TypedFieldMapper`` implementation.
Doctrine Mapping Types
----------------------
The ``type`` option used in the ``@Column`` accepts any of the existing
Doctrine types or even your own custom types. A Doctrine type defines
The ``type`` option used in the ``@Column`` accepts any of the
`existing Doctrine DBAL types <https://docs.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/types.html#reference>`_
or :doc:`your own custom mapping types
<../cookbook/custom-mapping-types>`. A Doctrine type defines
the conversion between PHP and SQL types, independent from the database vendor
you are using. All Mapping Types that ship with Doctrine are fully portable
between the supported database systems.
As an example, the Doctrine Mapping Type ``string`` defines the
mapping from a PHP string to a SQL VARCHAR (or VARCHAR2 etc.
depending on the RDBMS brand). Here is a quick overview of the
built-in mapping types:
- ``string``: Type that maps a SQL VARCHAR to a PHP string.
- ``integer``: Type that maps a SQL INT to a PHP integer.
- ``smallint``: Type that maps a database SMALLINT to a PHP
integer.
- ``bigint``: Type that maps a database BIGINT to a PHP string.
- ``boolean``: Type that maps a SQL boolean or equivalent (TINYINT) to a PHP boolean.
- ``decimal``: Type that maps a SQL DECIMAL to a PHP string.
- ``date``: Type that maps a SQL DATETIME to a PHP DateTime
object.
- ``time``: Type that maps a SQL TIME to a PHP DateTime object.
- ``datetime``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP
DateTime object.
- ``datetimetz``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP
DateTime object with timezone.
- ``text``: Type that maps a SQL CLOB to a PHP string.
- ``object``: Type that maps a SQL CLOB to a PHP object using
``serialize()`` and ``unserialize()``
- ``array``: Type that maps a SQL CLOB to a PHP array using
``serialize()`` and ``unserialize()``
- ``simple_array``: Type that maps a SQL CLOB to a PHP array using
``implode()`` and ``explode()``, with a comma as delimiter. *IMPORTANT*
Only use this type if you are sure that your values cannot contain a ",".
- ``json_array``: Type that maps a SQL CLOB to a PHP array using
``json_encode()`` and ``json_decode()``
- ``float``: Type that maps a SQL Float (Double Precision) to a
PHP double. *IMPORTANT*: Works only with locale settings that use
decimal points as separator.
- ``guid``: Type that maps a database GUID/UUID to a PHP string. Defaults to
varchar but uses a specific type if the platform supports it.
- ``blob``: Type that maps a SQL BLOB to a PHP resource stream
A cookbook article shows how to define :doc:`your own custom mapping types
<../cookbook/custom-mapping-types>`.
you are using.
.. note::

View File

@@ -1003,7 +1003,7 @@ The Query class
---------------
An instance of the ``Doctrine\ORM\Query`` class represents a DQL
query. You create a Query instance be calling
query. You create a Query instance by calling
``EntityManager#createQuery($dql)``, passing the DQL query string.
Alternatively you can create an empty ``Query`` instance and invoke
``Query#setDQL($dql)`` afterwards. Here are some examples:
@@ -1020,58 +1020,146 @@ Alternatively you can create an empty ``Query`` instance and invoke
$q = $em->createQuery();
$q->setDQL('select u from MyProject\Model\User u');
Query Result Formats
~~~~~~~~~~~~~~~~~~~~
Query Result Formats (Hydration Modes)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The format in which the result of a DQL SELECT query is returned
can be influenced by a so-called ``hydration mode``. A hydration
mode specifies a particular way in which a SQL result set is
transformed. Each hydration mode has its own dedicated method on
the Query class. Here they are:
The way in which the SQL result set of a DQL SELECT query is transformed
to PHP is determined by the so-called "hydration mode".
``getResult()``
^^^^^^^^^^^^^^^
- ``Query#getResult()``: Retrieves a collection of objects. The
result is either a plain collection of objects (pure) or an array
where the objects are nested in the result rows (mixed).
- ``Query#getSingleResult()``: Retrieves a single object. If the
result contains more than one object, an ``NonUniqueResultException``
is thrown. If the result contains no objects, an ``NoResultException``
is thrown. The pure/mixed distinction does not apply.
- ``Query#getOneOrNullResult()``: Retrieve a single object. If the
result contains more than one object, a ``NonUniqueResultException``
is thrown. If no object is found null will be returned.
- ``Query#getArrayResult()``: Retrieves an array graph (a nested
array) that is largely interchangeable with the object graph
generated by ``Query#getResult()`` for read-only purposes.
Retrieves a collection of objects. The result is either a plain collection of objects (pure) or an array
where the objects are nested in the result rows (mixed):
.. note::
.. code-block:: php
An array graph can differ from the corresponding object
graph in certain scenarios due to the difference of the identity
semantics between arrays and objects.
<?php
use Doctrine\ORM\AbstractQuery;
$query = $em->createQuery('SELECT u FROM User u');
$users = $query->getResult();
// same as:
$users = $query->getResult(AbstractQuery::HYDRATE_OBJECT);
- Objects fetched in a FROM clause are returned as a Set, that means every
object is only ever included in the resulting array once. This is the case
even when using JOIN or GROUP BY in ways that return the same row for an
object multiple times. If the hydrator sees the same object multiple times,
then it makes sure it is only returned once.
- ``Query#getScalarResult()``: Retrieves a flat/rectangular result
set of scalar values that can contain duplicate data. The
pure/mixed distinction does not apply.
- ``Query#getSingleScalarResult()``: Retrieves a single scalar
value from the result returned by the dbms. If the result contains
more than a single scalar value, an exception is thrown. The
pure/mixed distinction does not apply.
- If an object is already in memory from a previous query of any kind, then
then the previous object is used, even if the database may contain more
recent data. This even happens if the previous object is still an unloaded proxy.
Instead of using these methods, you can alternatively use the
general-purpose method
``Query#execute(array $params = [], $hydrationMode = Query::HYDRATE_OBJECT)``.
Using this method you can directly supply the hydration mode as the
second parameter via one of the Query constants. In fact, the
methods mentioned earlier are just convenient shortcuts for the
execute method. For example, the method ``Query#getResult()``
internally invokes execute, passing in ``Query::HYDRATE_OBJECT`` as
the hydration mode.
``getArrayResult()``
^^^^^^^^^^^^^^^^^^^^
The use of the methods mentioned earlier is generally preferred as
it leads to more concise code.
Retrieves an array graph (a nested array) for read-only purposes.
.. note::
An array graph can differ from the corresponding object
graph in certain scenarios due to the difference of the identity
semantics between arrays and objects.
.. code-block:: php
<?php
$users = $query->getArrayResult();
// same as:
$users = $query->getResult(AbstractQuery::HYDRATE_ARRAY);
``getScalarResult()``
^^^^^^^^^^^^^^^^^^^^^
Retrieves a flat/rectangular result set of scalar values that can contain duplicate data. The
pure/mixed distinction does not apply.
.. code-block:: php
<?php
$users = $query->getScalarResult();
// same as:
$users = $query->getResult(AbstractQuery::HYDRATE_SCALAR);
Fields from classes are prefixed by the DQL alias in the result.
A query of the kind `SELECT u.name ...` returns a key `u_name` in the result rows.
``getSingleScalarResult()``
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Retrieves a single scalar value from the result returned by the database. If the result contains
more than a single scalar value, a ``NonUniqueResultException`` is thrown. The pure/mixed distinction does not apply.
.. code-block:: php
<?php
$query = $em->createQuery('SELECT COUNT(u.id) FROM User u');
$numUsers = $query->getSingleScalarResult();
// same as:
$numUsers = $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR);
``getSingleColumnResult()``
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Retrieves an array from a one-dimensional array of scalar values:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT a.id FROM User u');
$ids = $query->getSingleColumnResult();
// same as:
$ids = $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN);
``getSingleResult()``
^^^^^^^^^^^^^^^^^^^^^
Retrieves a single object. If the result contains more than one object, a ``NonUniqueResultException``
is thrown. If the result contains no objects, a ``NoResultException`` is thrown. The pure/mixed distinction does not apply.
``getOneOrNullResult()``
^^^^^^^^^^^^^^^^^^^^^^^^
Retrieves a single object. If the result contains more than one object, a ``NonUniqueResultException``
is thrown. If no object is found, ``null`` will be returned.
Custom Hydration Modes
^^^^^^^^^^^^^^^^^^^^^^
You can easily add your own custom hydration modes by first
creating a class which extends ``AbstractHydrator``:
.. code-block:: php
<?php
namespace MyProject\Hydrators;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
class CustomHydrator extends AbstractHydrator
{
protected function _hydrateAll()
{
return $this->_stmt->fetchAllAssociative();
}
}
Next you just need to add the class to the ORM configuration:
.. code-block:: php
<?php
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'MyProject\Hydrators\CustomHydrator');
Now the hydrator is ready to be used in your queries:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$results = $query->getResult('CustomHydrator');
Pure and Mixed Results
~~~~~~~~~~~~~~~~~~~~~~
@@ -1175,165 +1263,6 @@ will return the rows iterating the different top-level entities.
[2] => Object (User)
[3] => Object (Group)
Hydration Modes
~~~~~~~~~~~~~~~
Each of the Hydration Modes makes assumptions about how the result
is returned to user land. You should know about all the details to
make best use of the different result formats:
The constants for the different hydration modes are:
- ``Query::HYDRATE_OBJECT``
- ``Query::HYDRATE_ARRAY``
- ``Query::HYDRATE_SCALAR``
- ``Query::HYDRATE_SINGLE_SCALAR``
- ``Query::HYDRATE_SCALAR_COLUMN``
Object Hydration
^^^^^^^^^^^^^^^^
Object hydration hydrates the result set into the object graph:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$users = $query->getResult(Query::HYDRATE_OBJECT);
Sometimes the behavior in the object hydrator can be confusing, which is
why we are listing as many of the assumptions here for reference:
- Objects fetched in a FROM clause are returned as a Set, that means every
object is only ever included in the resulting array once. This is the case
even when using JOIN or GROUP BY in ways that return the same row for an
object multiple times. If the hydrator sees the same object multiple times,
then it makes sure it is only returned once.
- If an object is already in memory from a previous query of any kind, then
then the previous object is used, even if the database may contain more
recent data. Data from the database is discarded. This even happens if the
previous object is still an unloaded proxy.
This list might be incomplete.
Array Hydration
^^^^^^^^^^^^^^^
You can run the same query with array hydration and the result set
is hydrated into an array that represents the object graph:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$users = $query->getResult(Query::HYDRATE_ARRAY);
You can use the ``getArrayResult()`` shortcut as well:
.. code-block:: php
<?php
$users = $query->getArrayResult();
Scalar Hydration
^^^^^^^^^^^^^^^^
If you want to return a flat rectangular result set instead of an
object graph you can use scalar hydration:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$users = $query->getResult(Query::HYDRATE_SCALAR);
echo $users[0]['u_id'];
The following assumptions are made about selected fields using
Scalar Hydration:
1. Fields from classes are prefixed by the DQL alias in the result.
A query of the kind 'SELECT u.name ..' returns a key 'u_name' in
the result rows.
Single Scalar Hydration
^^^^^^^^^^^^^^^^^^^^^^^
If you have a query which returns just a single scalar value you can use
single scalar hydration:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT COUNT(a.id) FROM CmsUser u LEFT JOIN u.articles a WHERE u.username = ?1 GROUP BY u.id');
$query->setParameter(1, 'jwage');
$numArticles = $query->getResult(Query::HYDRATE_SINGLE_SCALAR);
You can use the ``getSingleScalarResult()`` shortcut as well:
.. code-block:: php
<?php
$numArticles = $query->getSingleScalarResult();
Scalar Column Hydration
^^^^^^^^^^^^^^^^^^^^^^^
If you have a query which returns a one-dimensional array of scalar values
you can use scalar column hydration:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT a.id FROM CmsUser u');
$ids = $query->getResult(Query::HYDRATE_SCALAR_COLUMN);
You can use the ``getSingleColumnResult()`` shortcut as well:
.. code-block:: php
<?php
$ids = $query->getSingleColumnResult();
Custom Hydration Modes
^^^^^^^^^^^^^^^^^^^^^^
You can easily add your own custom hydration modes by first
creating a class which extends ``AbstractHydrator``:
.. code-block:: php
<?php
namespace MyProject\Hydrators;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
class CustomHydrator extends AbstractHydrator
{
protected function _hydrateAll()
{
return $this->_stmt->fetchAllAssociative();
}
}
Next you just need to add the class to the ORM configuration:
.. code-block:: php
<?php
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'MyProject\Hydrators\CustomHydrator');
Now the hydrator is ready to be used in your queries:
.. code-block:: php
<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$results = $query->getResult('CustomHydrator');
Iterating Large Result Sets
~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -88,7 +88,7 @@ requirement.
A more convenient alternative for explicit transaction demarcation is the use
of provided control abstractions in the form of
``Connection#transactional($func)`` and ``EntityManager#transactional($func)``.
``Connection#transactional($func)`` and ``EntityManager#wrapInTransaction($func)``.
When used, these control abstractions ensure that you never forget to rollback
the transaction, in addition to the obvious code reduction. An example that is
functionally equivalent to the previously shown code looks as follows:
@@ -96,21 +96,23 @@ functionally equivalent to the previously shown code looks as follows:
.. code-block:: php
<?php
// transactional with Connection instance
// $conn instanceof Connection
$conn->transactional(function($conn) {
// ... do some work
$user = new User;
$user->setName('George');
});
// transactional with EntityManager instance
// $em instanceof EntityManager
$em->transactional(function($em) {
$em->wrapInTransaction(function($em) {
// ... do some work
$user = new User;
$user->setName('George');
$em->persist($user);
});
.. warning::
For historical reasons, ``EntityManager#transactional($func)`` will return
``true`` whenever the return value of ``$func`` is loosely false.
Some examples of this include ``array()``, ``"0"``, ``""``, ``0``, and
``null``.
The difference between ``Connection#transactional($func)`` and
``EntityManager#transactional($func)`` is that the latter
abstraction flushes the ``EntityManager`` prior to transaction

View File

@@ -188,7 +188,7 @@ We keep up the example of an Article with arbitrary attributes, the mapping look
#[OneToMany(targetEntity: ArticleAttribute::class, mappedBy: 'article', cascade: ['ALL'], indexBy: 'attribute')]
private Collection $attributes;
public function addAttribute(string $name, ArticleAttribute $value): void
public function addAttribute(string $name, string $value): void
{
$this->attributes[$name] = new ArticleAttribute($name, $value, $this);
}

View File

@@ -144,7 +144,7 @@ step:
// Create a simple "default" Doctrine ORM configuration for Attributes
$config = ORMSetup::createAttributeMetadataConfiguration(
paths: array(__DIR__."/src"),
paths: [__DIR__ . '/src'],
isDevMode: true,
);
// or if you prefer annotation, YAML or XML
@@ -153,7 +153,7 @@ step:
// isDevMode: true,
// );
// $config = ORMSetup::createXMLMetadataConfiguration(
// paths: array(__DIR__."/config/xml"),
// paths: [__DIR__ . '/config/xml'],
// isDevMode: true,
//);
// $config = ORMSetup::createYAMLMetadataConfiguration(

View File

@@ -14,7 +14,6 @@
<file>src</file>
<file>tests</file>
<exclude-pattern>*/src/Mapping/InverseJoinColumn.php</exclude-pattern>
<exclude-pattern>*/tests/Tests/Proxies/__CG__*</exclude-pattern>
<exclude-pattern>*/tests/Tests/ORM/Tools/Export/export/*</exclude-pattern>

View File

@@ -135,11 +135,6 @@ parameters:
count: 2
path: src/EntityManager.php
-
message: "#^Template type T of method Doctrine\\\\ORM\\\\EntityManagerInterface\\:\\:getClassMetadata\\(\\) is not referenced in a parameter\\.$#"
count: 1
path: src/EntityManagerInterface.php
-
message: "#^Method Doctrine\\\\ORM\\\\EntityRepository\\:\\:matching\\(\\) should return Doctrine\\\\Common\\\\Collections\\\\AbstractLazyCollection\\<int, T of object\\>&Doctrine\\\\Common\\\\Collections\\\\Selectable\\<int, T of object\\> but returns Doctrine\\\\ORM\\\\LazyCriteriaCollection\\<\\(int\\|string\\), object\\>\\.$#"
count: 1

View File

@@ -74,3 +74,9 @@ parameters:
-
message: '#^Call to method injectObjectManager\(\) on an unknown class Doctrine\\Persistence\\ObjectManagerAware\.$#'
path: src/UnitOfWork.php
-
message: '#contains generic type.*but class.*is not generic#'
paths:
- src/Mapping/Driver/XmlDriver.php
- src/Mapping/Driver/YamlDriver.php

View File

@@ -64,3 +64,9 @@ parameters:
# Symfony cache supports passing a key prefix to the clear method.
- '/^Method Psr\\Cache\\CacheItemPoolInterface\:\:clear\(\) invoked with 1 parameter, 0 required\.$/'
-
message: '#contains generic type.*but class.*is not generic#'
paths:
- src/Mapping/Driver/XmlDriver.php
- src/Mapping/Driver/YamlDriver.php

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.22.2@d768d914152dbbf3486c36398802f74e80cfde48">
<files psalm-version="5.24.0@462c80e31c34e58cc4f750c656be3927e80e550e">
<file src="src/AbstractQuery.php">
<DeprecatedClass>
<code><![CDATA[IterableResult]]></code>
@@ -257,12 +257,6 @@
<code><![CDATA[transactional]]></code>
<code><![CDATA[transactional]]></code>
</DeprecatedMethod>
<InvalidReturnStatement>
<code><![CDATA[$this->wrapped->getClassMetadata($className)]]></code>
</InvalidReturnStatement>
<InvalidReturnType>
<code><![CDATA[getClassMetadata]]></code>
</InvalidReturnType>
<MissingParamType>
<code><![CDATA[$entity]]></code>
<code><![CDATA[$lockMode]]></code>
@@ -309,11 +303,9 @@
<code><![CDATA[$entity instanceof $class->name ? $entity : null]]></code>
<code><![CDATA[$persister->load($sortedId, null, null, [], $lockMode)]]></code>
<code><![CDATA[$persister->loadById($sortedId)]]></code>
<code><![CDATA[$this->metadataFactory->getMetadataFor($className)]]></code>
</InvalidReturnStatement>
<InvalidReturnType>
<code><![CDATA[?T]]></code>
<code><![CDATA[getClassMetadata]]></code>
</InvalidReturnType>
<MissingReturnType>
<code><![CDATA[wrapInTransaction]]></code>
@@ -388,6 +380,9 @@
<code><![CDATA[serialize]]></code>
<code><![CDATA[unserialize]]></code>
</MethodSignatureMustProvideReturnType>
<ParamNameMismatch>
<code><![CDATA[$serialized]]></code>
</ParamNameMismatch>
</file>
<file src="src/Id/TableGenerator.php">
<PossiblyFalseOperand>
@@ -462,9 +457,6 @@
</RedundantConditionGivenDocblockType>
</file>
<file src="src/Internal/Hydration/ObjectHydrator.php">
<InvalidArgument>
<code><![CDATA[$element]]></code>
</InvalidArgument>
<PossiblyFalseArgument>
<code><![CDATA[$index]]></code>
</PossiblyFalseArgument>
@@ -940,13 +932,8 @@
<InvalidPropertyAssignmentValue>
<code><![CDATA[$metadata->table]]></code>
</InvalidPropertyAssignmentValue>
<InvalidPropertyFetch>
<code><![CDATA[$xmlRoot->{'discriminator-column'}]]></code>
<code><![CDATA[$xmlRoot->{'discriminator-map'}]]></code>
</InvalidPropertyFetch>
<InvalidReturnStatement>
<code><![CDATA[$mapping]]></code>
<code><![CDATA[$result]]></code>
<code><![CDATA[[
'usage' => $usage,
'region' => $region,
@@ -970,7 +957,6 @@
* options?: array
* }]]></code>
<code><![CDATA[array{usage: int|null, region?: string}]]></code>
<code><![CDATA[loadMappingFile]]></code>
</InvalidReturnType>
<MissingParamType>
<code><![CDATA[$fileExtension]]></code>
@@ -979,15 +965,6 @@
<MoreSpecificImplementedParamType>
<code><![CDATA[$metadata]]></code>
</MoreSpecificImplementedParamType>
<NoInterfaceProperties>
<code><![CDATA[$xmlRoot->{'discriminator-column'}]]></code>
<code><![CDATA[$xmlRoot->{'discriminator-map'}]]></code>
</NoInterfaceProperties>
<TypeDoesNotContainType>
<code><![CDATA[$xmlRoot->getName() === 'embeddable']]></code>
<code><![CDATA[$xmlRoot->getName() === 'entity']]></code>
<code><![CDATA[$xmlRoot->getName() === 'mapped-superclass']]></code>
</TypeDoesNotContainType>
</file>
<file src="src/Mapping/Driver/YamlDriver.php">
<ArgumentTypeCoercion>
@@ -1019,38 +996,6 @@
<MoreSpecificReturnType>
<code><![CDATA[array{usage: int|null, region: string|null}]]></code>
</MoreSpecificReturnType>
<PossiblyUndefinedMethod>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
</PossiblyUndefinedMethod>
<UndefinedInterfaceMethod>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
<code><![CDATA[$element]]></code>
</UndefinedInterfaceMethod>
</file>
<file src="src/Mapping/Embedded.php">
<MissingParamType>
@@ -1492,6 +1437,7 @@
<code><![CDATA[getValue]]></code>
<code><![CDATA[setAccessible]]></code>
<code><![CDATA[setValue]]></code>
<code><![CDATA[setValue]]></code>
</PossiblyNullReference>
<TypeDoesNotContainType>
<code><![CDATA[$autoGenerate < 0]]></code>
@@ -1501,13 +1447,10 @@
<code><![CDATA[__wakeup]]></code>
</UndefinedInterfaceMethod>
<UndefinedMethod>
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
$initializer($object, $proxy);
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
$initializer($object, $identifier);
}, $skippedProperties)]]></code>
</UndefinedMethod>
<UndefinedVariable>
<code><![CDATA[$proxy]]></code>
</UndefinedVariable>
<UnresolvableInclude>
<code><![CDATA[require $fileName]]></code>
</UnresolvableInclude>
@@ -2356,9 +2299,6 @@
<InvalidReturnType>
<code><![CDATA[ObjectRepository]]></code>
</InvalidReturnType>
<TypeDoesNotContainType>
<code><![CDATA[$repository instanceof EntityRepository]]></code>
</TypeDoesNotContainType>
<UnsafeInstantiation>
<code><![CDATA[new $repositoryClassName($entityManager, $metadata)]]></code>
</UnsafeInstantiation>
@@ -2529,6 +2469,11 @@
<PossiblyNullArgument>
<code><![CDATA[$variableType]]></code>
</PossiblyNullArgument>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$fieldMapping['declaredField']]]></code>
<code><![CDATA[$fieldMapping['declaredField']]]></code>
<code><![CDATA[$fieldMapping['declaredField']]]></code>
</PossiblyUndefinedArrayOffset>
<PropertyNotSetInConstructor>
<code><![CDATA[$classToExtend]]></code>
</PropertyNotSetInConstructor>

View File

@@ -340,7 +340,7 @@ interface EntityManagerInterface extends ObjectManager
* @psalm-param string|class-string<T> $className
*
* @return Mapping\ClassMetadata
* @psalm-return Mapping\ClassMetadata<T>
* @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
*
* @psalm-template T of object
*/

View File

@@ -182,29 +182,31 @@ abstract class AbstractHydrator
$this->prepare();
while (true) {
$row = $this->statement()->fetchAssociative();
try {
while (true) {
$row = $this->statement()->fetchAssociative();
if ($row === false) {
$this->cleanup();
break;
}
$result = [];
$this->hydrateRowData($row, $result);
$this->cleanupAfterRowIteration();
if (count($result) === 1) {
if (count($resultSetMapping->indexByMap) === 0) {
yield end($result);
} else {
yield from $result;
if ($row === false) {
break;
}
$result = [];
$this->hydrateRowData($row, $result);
$this->cleanupAfterRowIteration();
if (count($result) === 1) {
if (count($resultSetMapping->indexByMap) === 0) {
yield end($result);
} else {
yield from $result;
}
} else {
yield $result;
}
} else {
yield $result;
}
} finally {
$this->cleanup();
}
}

View File

@@ -367,11 +367,15 @@ class ObjectHydrator extends AbstractHydrator
$parentObject = $this->resultPointers[$parentAlias];
} else {
// Parent object of relation not found, mark as not-fetched again
$element = $this->getEntity($data, $dqlAlias);
if (isset($nonemptyComponents[$dqlAlias])) {
$element = $this->getEntity($data, $dqlAlias);
// Update result pointer and provide initial fetch data for parent
$this->resultPointers[$dqlAlias] = $element;
$rowData['data'][$parentAlias][$relationField] = $element;
// Update result pointer and provide initial fetch data for parent
$this->resultPointers[$dqlAlias] = $element;
$rowData['data'][$parentAlias][$relationField] = $element;
} else {
$element = null;
}
// Mark as not-fetched again
unset($this->_hints['fetched'][$parentAlias][$relationField]);

View File

@@ -76,11 +76,11 @@ class LazyCriteriaCollection extends AbstractLazyCollection implements Selectabl
}
/**
* {@inheritDoc}
*
* Do an optimized search of an element
*
* @template TMaybeContained
* @param mixed $element The element to search for.
*
* @return bool TRUE if the collection contains $element, FALSE otherwise.
*/
public function contains($element)
{

View File

@@ -37,6 +37,8 @@ use function strtoupper;
* XmlDriver is a metadata driver that enables mapping through XML files.
*
* @link www.doctrine-project.org
*
* @template-extends FileDriver<SimpleXMLElement>
*/
class XmlDriver extends FileDriver
{
@@ -79,7 +81,6 @@ class XmlDriver extends FileDriver
public function loadMetadataForClass($className, PersistenceClassMetadata $metadata)
{
$xmlRoot = $this->getElement($className);
assert($xmlRoot instanceof SimpleXMLElement);
if ($xmlRoot->getName() === 'entity') {
if (isset($xmlRoot['repository-class'])) {
@@ -203,6 +204,7 @@ class XmlDriver extends FileDriver
];
if (isset($discrColumn['options'])) {
assert($discrColumn['options'] instanceof SimpleXMLElement);
$columnDef['options'] = $this->parseOptions($discrColumn['options']->children());
}
@@ -214,6 +216,7 @@ class XmlDriver extends FileDriver
// Evaluate <discriminator-map...>
if (isset($xmlRoot->{'discriminator-map'})) {
$map = [];
assert($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} instanceof SimpleXMLElement);
foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) {
$map[(string) $discrMapElement['value']] = (string) $discrMapElement['class'];
}

View File

@@ -31,6 +31,8 @@ use function substr;
* The YamlDriver reads the mapping metadata from yaml schema files.
*
* @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
*
* @template-extends FileDriver<array<string, mixed>>
*/
class YamlDriver extends FileDriver
{

View File

@@ -2,7 +2,6 @@
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;

View File

@@ -5,11 +5,31 @@ declare(strict_types=1);
namespace Doctrine\ORM;
use Doctrine\Common\Cache\Cache as CacheDriver;
use Doctrine\Persistence\ObjectRepository;
use Doctrine\ORM\Cache\Exception\InvalidResultCacheDriver;
use Doctrine\ORM\Cache\Exception\MetadataCacheNotConfigured;
use Doctrine\ORM\Cache\Exception\MetadataCacheUsesNonPersistentCache;
use Doctrine\ORM\Cache\Exception\QueryCacheNotConfigured;
use Doctrine\ORM\Cache\Exception\QueryCacheUsesNonPersistentCache;
use Doctrine\ORM\Exception\EntityManagerClosed;
use Doctrine\ORM\Exception\InvalidEntityRepository;
use Doctrine\ORM\Exception\InvalidHydrationMode;
use Doctrine\ORM\Exception\MismatchedEventManager;
use Doctrine\ORM\Exception\MissingIdentifierField;
use Doctrine\ORM\Exception\MissingMappingDriverImplementation;
use Doctrine\ORM\Exception\NamedNativeQueryNotFound;
use Doctrine\ORM\Exception\NamedQueryNotFound;
use Doctrine\ORM\Exception\ProxyClassesAlwaysRegenerating;
use Doctrine\ORM\Exception\UnexpectedAssociationValue;
use Doctrine\ORM\Exception\UnknownEntityNamespace;
use Doctrine\ORM\Exception\UnrecognizedIdentifierFields;
use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys;
use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
use Doctrine\ORM\Repository\Exception\InvalidMagicMethodCall;
use Doctrine\ORM\Tools\Exception\NotSupported;
use Exception;
use function get_debug_type;
use function implode;
use function sprintf;
/**
@@ -26,8 +46,7 @@ class ORMException extends Exception
*/
public static function missingMappingDriverImpl()
{
return new self("It's a requirement to specify a Metadata Driver and pass it " .
'to Doctrine\\ORM\\Configuration::setMetadataDriverImpl().');
return MissingMappingDriverImplementation::create();
}
/**
@@ -39,11 +58,11 @@ class ORMException extends Exception
*/
public static function namedQueryNotFound($queryName)
{
return new self('Could not find a named query by the name "' . $queryName . '"');
return NamedQueryNotFound::fromName($queryName);
}
/**
* @deprecated Use Doctrine\ORM\Exception\NamedQueryNotFound
* @deprecated Use Doctrine\ORM\Exception\NamedNativeQueryNotFound
*
* @param string $nativeQueryName
*
@@ -51,7 +70,7 @@ class ORMException extends Exception
*/
public static function namedNativeQueryNotFound($nativeQueryName)
{
return new self('Could not find a named native query by the name "' . $nativeQueryName . '"');
return NamedNativeQueryNotFound::fromName($nativeQueryName);
}
/**
@@ -63,7 +82,7 @@ class ORMException extends Exception
*/
public static function unrecognizedField($field)
{
return new self(sprintf('Unrecognized field: %s', $field));
return new UnrecognizedField(sprintf('Unrecognized field: %s', $field));
}
/**
@@ -78,7 +97,7 @@ class ORMException extends Exception
*/
public static function unexpectedAssociationValue($class, $association, $given, $expected)
{
return new self(sprintf('Found entity of type %s on association %s#%s, but expecting %s', $given, $class, $association, $expected));
return UnexpectedAssociationValue::create($class, $association, $given, $expected);
}
/**
@@ -91,7 +110,7 @@ class ORMException extends Exception
*/
public static function invalidOrientation($className, $field)
{
return new self('Invalid order by orientation specified for ' . $className . '#' . $field);
return InvalidOrientation::fromClassNameAndField($className, $field);
}
/**
@@ -101,7 +120,7 @@ class ORMException extends Exception
*/
public static function entityManagerClosed()
{
return new self('The EntityManager is closed.');
return EntityManagerClosed::create();
}
/**
@@ -113,7 +132,7 @@ class ORMException extends Exception
*/
public static function invalidHydrationMode($mode)
{
return new self(sprintf("'%s' is an invalid hydration mode.", $mode));
return InvalidHydrationMode::fromMode($mode);
}
/**
@@ -123,7 +142,7 @@ class ORMException extends Exception
*/
public static function mismatchedEventManager()
{
return new self('Cannot use different EventManager instances for EntityManager and Connection.');
return MismatchedEventManager::create();
}
/**
@@ -135,11 +154,11 @@ class ORMException extends Exception
*/
public static function findByRequiresParameter($methodName)
{
return new self("You need to pass a parameter to '" . $methodName . "'");
return InvalidMagicMethodCall::onMissingParameter($methodName);
}
/**
* @deprecated Doctrine\ORM\Repository\Exception\InvalidFindByCall
* @deprecated Doctrine\ORM\Repository\Exception\InvalidMagicMethodCall::becauseFieldNotFoundIn()
*
* @param string $entityName
* @param string $fieldName
@@ -149,10 +168,7 @@ class ORMException extends Exception
*/
public static function invalidMagicCall($entityName, $fieldName, $method)
{
return new self(
"Entity '" . $entityName . "' has no field '" . $fieldName . "'. " .
"You can therefore not call '" . $method . "' on the entities' repository"
);
return InvalidMagicMethodCall::becauseFieldNotFoundIn($entityName, $fieldName, $method);
}
/**
@@ -165,10 +181,7 @@ class ORMException extends Exception
*/
public static function invalidFindByInverseAssociation($entityName, $associationFieldName)
{
return new self(
"You cannot search for the association field '" . $entityName . '#' . $associationFieldName . "', " .
'because it is the inverse side of an association. Find methods only work on owning side associations.'
);
return InvalidFindByCall::fromInverseSideUsage($entityName, $associationFieldName);
}
/**
@@ -178,7 +191,7 @@ class ORMException extends Exception
*/
public static function invalidResultCacheDriver()
{
return new self('Invalid result cache driver; it must implement Doctrine\\Common\\Cache\\Cache.');
return InvalidResultCacheDriver::create();
}
/**
@@ -188,7 +201,7 @@ class ORMException extends Exception
*/
public static function notSupported()
{
return new self('This behaviour is (currently) not supported by Doctrine 2');
return NotSupported::create();
}
/**
@@ -198,7 +211,7 @@ class ORMException extends Exception
*/
public static function queryCacheNotConfigured()
{
return new self('Query Cache is not configured.');
return QueryCacheNotConfigured::create();
}
/**
@@ -208,7 +221,7 @@ class ORMException extends Exception
*/
public static function metadataCacheNotConfigured()
{
return new self('Class Metadata Cache is not configured.');
return MetadataCacheNotConfigured::create();
}
/**
@@ -218,7 +231,7 @@ class ORMException extends Exception
*/
public static function queryCacheUsesNonPersistentCache(CacheDriver $cache)
{
return new self('Query Cache uses a non-persistent cache driver, ' . get_debug_type($cache) . '.');
return QueryCacheUsesNonPersistentCache::fromDriver($cache);
}
/**
@@ -228,7 +241,7 @@ class ORMException extends Exception
*/
public static function metadataCacheUsesNonPersistentCache(CacheDriver $cache)
{
return new self('Metadata Cache uses a non-persistent cache driver, ' . get_debug_type($cache) . '.');
return MetadataCacheUsesNonPersistentCache::fromDriver($cache);
}
/**
@@ -238,7 +251,7 @@ class ORMException extends Exception
*/
public static function proxyClassesAlwaysRegenerating()
{
return new self('Proxy Classes are always regenerating.');
return ProxyClassesAlwaysRegenerating::create();
}
/**
@@ -250,9 +263,7 @@ class ORMException extends Exception
*/
public static function unknownEntityNamespace($entityNamespaceAlias)
{
return new self(
sprintf("Unknown Entity namespace alias '%s'.", $entityNamespaceAlias)
);
return UnknownEntityNamespace::fromNamespaceAlias($entityNamespaceAlias);
}
/**
@@ -264,11 +275,7 @@ class ORMException extends Exception
*/
public static function invalidEntityRepository($className)
{
return new self(sprintf(
"Invalid repository class '%s'. It must be a %s.",
$className,
ObjectRepository::class
));
return InvalidEntityRepository::fromClassName($className);
}
/**
@@ -281,7 +288,7 @@ class ORMException extends Exception
*/
public static function missingIdentifierField($className, $fieldName)
{
return new self(sprintf('The identifier %s is missing for a query of %s', $fieldName, $className));
return MissingIdentifierField::fromFieldAndClass($fieldName, $className);
}
/**
@@ -294,10 +301,7 @@ class ORMException extends Exception
*/
public static function unrecognizedIdentifierFields($className, $fieldNames)
{
return new self(
"Unrecognized identifier fields: '" . implode("', '", $fieldNames) . "' " .
"are not present on class '" . $className . "'."
);
return UnrecognizedIdentifierFields::fromClassAndFieldNames($className, $fieldNames);
}
/**
@@ -307,6 +311,6 @@ class ORMException extends Exception
*/
public static function cantUseInOperatorOnCompositeKeys()
{
return new self("Can't use IN operator on entities that have composite keys.");
return CantUseInOperatorOnCompositeKeys::create();
}
}

View File

@@ -412,11 +412,6 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
return parent::containsKey($key);
}
/**
* {@inheritDoc}
*
* @template TMaybeContained
*/
public function contains($element): bool
{
if (! $this->initialized && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) {

View File

@@ -8,6 +8,8 @@ use BadMethodCallException;
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Utility\PersisterHelper;
@@ -166,7 +168,11 @@ class OneToManyPersister extends AbstractCollectionPersister
throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
}
/** @throws DBALException */
/**
* @throws DBALException
* @throws EntityNotFoundException
* @throws MappingException
*/
private function deleteEntityCollection(PersistentCollection $collection): int
{
$mapping = $collection->getMapping();
@@ -186,6 +192,13 @@ class OneToManyPersister extends AbstractCollectionPersister
$statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
if ($targetClass->isInheritanceTypeSingleTable()) {
$discriminatorColumn = $targetClass->getDiscriminatorColumn();
$statement .= ' AND ' . $discriminatorColumn['name'] . ' = ?';
$parameters[] = $targetClass->discriminatorValue;
$types[] = $discriminatorColumn['type'];
}
$numAffected = $this->conn->executeStatement($statement, $parameters, $types);
assert(is_int($numAffected));

View File

@@ -354,17 +354,16 @@ EOPHP;
/**
* Creates a closure capable of initializing a proxy
*
* @return Closure(InternalProxy, InternalProxy):void
* @return Closure(InternalProxy, array):void
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
return static function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($original);
$entity = $entityPersister->loadById($identifier, $original);
return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$original = $entityPersister->loadById($identifier);
if ($entity === null) {
if ($original === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
$classMetadata->getName(),
$identifierFlattener->flattenIdentifier($classMetadata, $identifier)
@@ -378,11 +377,11 @@ EOPHP;
$class = $entityPersister->getClassMetadata();
foreach ($class->getReflectionProperties() as $property) {
if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
if (isset($identifier[$property->name]) || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
continue;
}
$property->setValue($proxy, $property->getValue($entity));
$property->setValue($proxy, $property->getValue($original));
}
};
}
@@ -468,8 +467,8 @@ EOPHP;
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
$initializer($object, $proxy);
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
$initializer($object, $identifier);
}, $skippedProperties);
foreach ($identifierFields as $idField => $reflector) {

View File

@@ -2924,7 +2924,10 @@ class Parser
return new AST\ParenthesisExpression($expr);
}
assert($this->lexer->lookahead !== null);
if ($this->lexer->lookahead === null) {
$this->syntaxError('ArithmeticPrimary');
}
switch ($this->lexer->lookahead->type) {
case TokenType::T_COALESCE:
case TokenType::T_NULLIF:

View File

@@ -1062,7 +1062,9 @@ class SqlWalker implements TreeWalker
}
}
if ($relation['fetch'] === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
$fetchMode = $this->query->getHint('fetchMode')[$assoc['sourceEntity']][$assoc['fieldName']] ?? $relation['fetch'];
if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
throw QueryException::eagerFetchJoinWithNotAllowed($assoc['sourceEntity'], $assoc['fieldName']);
}

View File

@@ -64,18 +64,18 @@ class SchemaValidator
* It maps built-in Doctrine types to PHP types
*/
private const BUILTIN_TYPES_MAP = [
AsciiStringType::class => 'string',
BigIntType::class => 'string',
BooleanType::class => 'bool',
DecimalType::class => 'string',
FloatType::class => 'float',
GuidType::class => 'string',
IntegerType::class => 'int',
JsonType::class => 'array',
SimpleArrayType::class => 'array',
SmallIntType::class => 'int',
StringType::class => 'string',
TextType::class => 'string',
AsciiStringType::class => ['string'],
BigIntType::class => ['int', 'string'],
BooleanType::class => ['bool'],
DecimalType::class => ['string'],
FloatType::class => ['float'],
GuidType::class => ['string'],
IntegerType::class => ['int'],
JsonType::class => ['array'],
SimpleArrayType::class => ['array'],
SmallIntType::class => ['int'],
StringType::class => ['string'],
TextType::class => ['string'],
];
public function __construct(EntityManagerInterface $em, bool $validatePropertyTypes = true)
@@ -390,21 +390,21 @@ class SchemaValidator
$propertyType = $propertyType->getName();
// If the property type is the same as the metadata field type, we are ok
if ($propertyType === $metadataFieldType) {
if (in_array($propertyType, $metadataFieldType, true)) {
return null;
}
if (is_a($propertyType, BackedEnum::class, true)) {
$backingType = (string) (new ReflectionEnum($propertyType))->getBackingType();
if ($metadataFieldType !== $backingType) {
if (! in_array($backingType, $metadataFieldType, true)) {
return sprintf(
"The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
$class->name,
$fieldName,
$propertyType,
$backingType,
$metadataFieldType
implode('|', $metadataFieldType)
);
}
@@ -429,7 +429,7 @@ class SchemaValidator
) {
$backingType = (string) (new ReflectionEnum($fieldMapping['enumType']))->getBackingType();
if ($metadataFieldType === $backingType) {
if (in_array($backingType, $metadataFieldType, true)) {
return null;
}
@@ -439,7 +439,7 @@ class SchemaValidator
$fieldName,
$fieldMapping['enumType'],
$backingType,
$metadataFieldType
implode('|', $metadataFieldType)
);
}
@@ -455,7 +455,7 @@ class SchemaValidator
$class->name,
$fieldName,
$propertyType,
$metadataFieldType,
implode('|', $metadataFieldType),
$fieldMapping['type']
);
},
@@ -468,8 +468,10 @@ class SchemaValidator
/**
* The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own
* customization around field types.
*
* @return list<string>|null
*/
private function findBuiltInType(Type $type): ?string
private function findBuiltInType(Type $type): ?array
{
$typeName = get_class($type);

View File

@@ -1292,6 +1292,8 @@ class UnitOfWork implements PropertyChangedListener
$eventsToDispatch = [];
foreach ($entities as $entity) {
$this->removeFromIdentityMap($entity);
$oid = spl_object_id($entity);
$class = $this->em->getClassMetadata(get_class($entity));
$persister = $this->getEntityPersister($class->name);
@@ -1667,8 +1669,6 @@ class UnitOfWork implements PropertyChangedListener
return;
}
$this->removeFromIdentityMap($entity);
unset($this->entityUpdates[$oid]);
if (! isset($this->entityDeletions[$oid])) {
@@ -3053,10 +3053,7 @@ EXCEPTION
} else {
$associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
}
} elseif (
$targetClass->containsForeignIdentifier
&& in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
) {
} elseif (in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)) {
// the missing key is part of target's entity primary key
$associatedId = [];
break;
@@ -3166,7 +3163,7 @@ EXCEPTION
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
$isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
if ($assoc['type'] === ClassMetadata::ONE_TO_MANY && ! $isIteration && ! $targetClass->isIdentifierComposite) {
if ($assoc['type'] === ClassMetadata::ONE_TO_MANY && ! $isIteration && ! $targetClass->isIdentifierComposite && ! isset($assoc['indexBy'])) {
$this->scheduleCollectionForBatchLoading($pColl, $class);
} else {
$this->loadCollection($pColl);
@@ -3224,7 +3221,13 @@ EXCEPTION
*
* @param PersistentCollection[] $collections
* @param array<string, mixed> $mapping
* @psalm-param array{targetEntity: class-string, sourceEntity: class-string, mappedBy: string, indexBy: string|null} $mapping
* @psalm-param array{
* targetEntity: class-string,
* sourceEntity: class-string,
* mappedBy: string,
* indexBy: string|null,
* orderBy: array<string, string>|null
* } $mapping
*/
private function eagerLoadCollections(array $collections, array $mapping): void
{
@@ -3241,7 +3244,7 @@ EXCEPTION
$entities[] = $collection->getOwner();
}
$found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities]);
$found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping['orderBy'] ?? null);
$targetClass = $this->em->getClassMetadata($targetEntity);
$targetProperty = $targetClass->getReflectionProperty($mappedBy);

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\BigIntegers;
use Doctrine\ORM\Mapping as ORM;
/** @ORM\Entity */
class BigIntegers
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
public ?int $id = null;
/** @ORM\Column(type="bigint") */
public int $one = 1;
/** @ORM\Column(type="bigint") */
public string $two = '2';
/** @ORM\Column(type="bigint") */
public float $three = 3.0;
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\CompositeKeyRelations;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
/** @Entity */
class CustomerClass
{
/**
* @var string
* @Id
* @Column(type="string")
*/
public $companyCode;
/**
* @var string
* @Id
* @Column(type="string")
*/
public $code;
/**
* @var string
* @Column(type="string")
*/
public $name;
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\CompositeKeyRelations;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\JoinColumns;
use Doctrine\ORM\Mapping\ManyToOne;
/** @Entity */
class InvoiceClass
{
/**
* @var string
* @Id
* @Column(type="string")
*/
public $companyCode;
/**
* @var string
* @Id
* @Column(type="string")
*/
public $invoiceNumber;
/**
* @var CustomerClass|null
* @ManyToOne(targetEntity="CustomerClass")
* @JoinColumns({
* @JoinColumn(name="companyCode", referencedColumnName="companyCode"),
* @JoinColumn(name="customerCode", referencedColumnName="code")
* })
*/
public $customer;
/**
* @var string|null
* @Column(type="string", nullable=true)
*/
public $customerCode;
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\ECommerce;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Index;
use Doctrine\ORM\Mapping\Table;
/**
* ECommerceProduct2
* Resets the id when being cloned.
*
* @Entity
* @Table(name="ecommerce_products",indexes={@Index(name="name_idx", columns={"name"})})
*/
class ECommerceProduct2
{
/**
* @var int|null
* @Column(type="integer")
* @Id
* @GeneratedValue
*/
private $id;
/**
* @var string
* @Column(type="string", length=50, nullable=true)
*/
private $name;
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function __clone()
{
$this->id = null;
$this->name = 'Clone of ' . $this->name;
}
}

View File

@@ -6,6 +6,7 @@ namespace Doctrine\Tests\ORM\Cache\Persister\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\Cache\Persister\Entity\AbstractEntityPersister;
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
@@ -99,7 +100,7 @@ abstract class EntityPersisterTestCase extends OrmTestCase
->with(
self::identicalTo(['name' => 'Foo']),
self::identicalTo([0]),
self::identicalTo(1),
self::identicalTo(LockMode::OPTIMISTIC),
self::identicalTo(2),
self::identicalTo(3),
self::identicalTo([4])
@@ -109,7 +110,7 @@ abstract class EntityPersisterTestCase extends OrmTestCase
self::assertSame('SELECT * FROM foo WERE name = ?', $persister->getSelectSQL(
['name' => 'Foo'],
[0],
1,
LockMode::OPTIMISTIC,
2,
3,
[4]
@@ -228,13 +229,21 @@ abstract class EntityPersisterTestCase extends OrmTestCase
self::identicalTo($entity),
self::identicalTo([0]),
self::identicalTo([1]),
self::identicalTo(2),
self::identicalTo(LockMode::PESSIMISTIC_READ),
self::identicalTo(3),
self::identicalTo([4])
)
->willReturn($entity);
self::assertSame($entity, $persister->load(['id' => 1], $entity, [0], [1], 2, 3, [4]));
self::assertSame($entity, $persister->load(
['id' => 1],
$entity,
[0],
[1],
LockMode::PESSIMISTIC_READ,
3,
[4]
));
}
public function testInvokeLoadAll(): void
@@ -391,9 +400,9 @@ abstract class EntityPersisterTestCase extends OrmTestCase
$this->entityPersister->expects(self::once())
->method('lock')
->with(self::identicalTo($identifier), self::identicalTo(1));
->with(self::identicalTo($identifier), self::identicalTo(LockMode::OPTIMISTIC));
$persister->lock($identifier, 1);
$persister->lock($identifier, LockMode::OPTIMISTIC);
}
public function testInvokeExists(): void

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Tests\Models\CompositeKeyRelations\CustomerClass;
use Doctrine\Tests\Models\CompositeKeyRelations\InvoiceClass;
use Doctrine\Tests\OrmFunctionalTestCase;
class CompositeKeyRelationsTest extends OrmFunctionalTestCase
{
protected function setUp(): void
{
$this->useModelSet('compositekeyrelations');
parent::setUp();
}
public function testFindEntityWithNotNullRelation(): void
{
$this->_em->getConnection()->insert('CustomerClass', [
'companyCode' => 'AA',
'code' => 'CUST1',
'name' => 'Customer 1',
]);
$this->_em->getConnection()->insert('InvoiceClass', [
'companyCode' => 'AA',
'invoiceNumber' => 'INV1',
'customerCode' => 'CUST1',
]);
$entity = $this->findEntity('AA', 'INV1');
self::assertSame('AA', $entity->companyCode);
self::assertSame('INV1', $entity->invoiceNumber);
self::assertInstanceOf(CustomerClass::class, $entity->customer);
self::assertSame('Customer 1', $entity->customer->name);
}
public function testFindEntityWithNullRelation(): void
{
$this->_em->getConnection()->insert('InvoiceClass', [
'companyCode' => 'BB',
'invoiceNumber' => 'INV1',
]);
$entity = $this->findEntity('BB', 'INV1');
self::assertSame('BB', $entity->companyCode);
self::assertSame('INV1', $entity->invoiceNumber);
self::assertNull($entity->customer);
}
private function findEntity(string $companyCode, string $invoiceNumber): InvoiceClass
{
return $this->_em->find(
InvoiceClass::class,
['companyCode' => $companyCode, 'invoiceNumber' => $invoiceNumber]
);
}
}

View File

@@ -88,6 +88,14 @@ class EagerFetchCollectionTest extends OrmFunctionalTestCase
$query->getResult();
}
public function testSubselectFetchJoinWithAllowedWhenOverriddenNotEager(): void
{
$query = $this->_em->createQuery('SELECT o, c FROM ' . EagerFetchOwner::class . ' o JOIN o.children c WITH c.id = 1');
$query->setFetchMode(EagerFetchChild::class, 'owner', ORM\ClassMetadata::FETCH_LAZY);
$this->assertIsString($query->getSql());
}
public function testEagerFetchWithIterable(): void
{
$this->createOwnerWithChildren(2);

View File

@@ -58,7 +58,7 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
public function testPersistUpdate(): void
{
// Considering case (a)
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => 123]);
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => $this->user->getId()]);
$proxy->id = null;
$proxy->username = 'ocra';

View File

@@ -249,7 +249,6 @@ class QueryDqlFunctionTest extends OrmFunctionalTestCase
self::assertEquals(1600000, $result[3]['op']);
}
/** @group test */
public function testOperatorDiv(): void
{
$result = $this->_em->createQuery('SELECT m, (m.salary/0.5) AS op FROM Doctrine\Tests\Models\Company\CompanyManager m ORDER BY m.salary ASC')

View File

@@ -9,6 +9,7 @@ use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\Tests\Models\Company\CompanyAuction;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct2;
use Doctrine\Tests\Models\ECommerce\ECommerceShipping;
use Doctrine\Tests\OrmFunctionalTestCase;
@@ -112,6 +113,24 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
self::assertFalse($entity->isCloned);
}
public function testCloneProxyWithResetId(): void
{
$id = $this->createProduct();
$entity = $this->_em->getReference(ECommerceProduct2::class, $id);
assert($entity instanceof ECommerceProduct2);
$clone = clone $entity;
assert($clone instanceof ECommerceProduct2);
self::assertEquals($id, $entity->getId());
self::assertEquals('Doctrine Cookbook', $entity->getName());
self::assertFalse($this->_em->contains($clone));
self::assertNull($clone->getId());
self::assertEquals('Clone of Doctrine Cookbook', $clone->getName());
}
/** @group DDC-733 */
public function testInitializeProxy(): void
{

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @see https://github.com/doctrine/orm/issues/10889
*
* @group GH10889
*/
class GH10889Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH10889Person::class,
GH10889Company::class,
GH10889Resume::class
);
}
public function testIssue(): void
{
$person = new GH10889Person();
$resume = new GH10889Resume($person, null);
$this->_em->persist($person);
$this->_em->persist($resume);
$this->_em->flush();
$this->_em->clear();
/** @var list<GH10889Resume> $resumes */
$resumes = $this->_em
->getRepository(GH10889Resume::class)
->createQueryBuilder('resume')
->leftJoin('resume.currentCompany', 'company')->addSelect('company')
->getQuery()
->getResult();
$this->assertArrayHasKey(0, $resumes);
$this->assertEquals(1, $resumes[0]->person->id);
$this->assertNull($resumes[0]->currentCompany);
}
}
/**
* @ORM\Entity
*/
class GH10889Person
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
}
/**
* @ORM\Entity
*/
class GH10889Company
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
}
/**
* @ORM\Entity
*/
class GH10889Resume
{
/**
* @ORM\Id
* @ORM\OneToOne(targetEntity="GH10889Person")
*
* @var GH10889Person
*/
public $person;
/**
* @ORM\ManyToOne(targetEntity="GH10889Company")
*
* @var GH10889Company|null
*/
public $currentCompany;
public function __construct(GH10889Person $person, ?GH10889Company $currentCompany)
{
$this->person = $person;
$this->currentCompany = $currentCompany;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("gh11149_eager_product")
*/
class EagerProduct
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(
* targetEntity=EagerProductTranslation::class,
* mappedBy="product",
* fetch="EAGER",
* indexBy="locale_code"
* )
*
* @var Collection<string, EagerProductTranslation>
*/
public $translations;
public function __construct(int $id)
{
$this->id = $id;
$this->translations = new ArrayCollection();
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("gh11149_eager_product_translation")
*/
class EagerProductTranslation
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
*
* @var int
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=EagerProduct::class, inversedBy="translations")
* @ORM\JoinColumn(nullable=false)
*
* @var EagerProduct
*/
public $product;
/**
* @ORM\ManyToOne(targetEntity=Locale::class)
* @ORM\JoinColumn(name="locale_code", referencedColumnName="code", nullable=false)
*
* @var Locale
*/
public $locale;
public function __construct($id, EagerProduct $product, Locale $locale)
{
$this->id = $id;
$this->product = $product;
$this->locale = $locale;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH11149Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
Locale::class,
EagerProduct::class,
EagerProductTranslation::class,
]);
}
public function testFetchEagerModeWithIndexBy(): void
{
// Load entities into database
$this->_em->persist($product = new EagerProduct(11149));
$this->_em->persist($locale = new Locale('fr_FR'));
$this->_em->persist(new EagerProductTranslation(11149, $product, $locale));
$this->_em->flush();
$this->_em->clear();
// Fetch entity from database
$product = $this->_em->find(EagerProduct::class, 11149);
// Assert associated entity is loaded eagerly
static::assertInstanceOf(EagerProduct::class, $product);
static::assertInstanceOf(PersistentCollection::class, $product->translations);
static::assertTrue($product->translations->isInitialized());
static::assertCount(1, $product->translations);
// Assert associated entity is indexed by given property
$translation = $product->translations->get('fr_FR');
static::assertInstanceOf(EagerProductTranslation::class, $translation);
static::assertNotInstanceOf(Proxy::class, $translation);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("gh11149_locale")
*/
class Locale
{
/**
* @ORM\Id
* @ORM\Column(type="string", length=5)
*
* @var string
*/
public $code;
public function __construct(string $code)
{
$this->code = $code;
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH11163Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH11163Bucket::class,
GH11163BucketItem::class,
]);
}
public function tearDown(): void
{
parent::tearDown();
$conn = static::$sharedConn;
$conn->executeStatement('DELETE FROM GH11163BucketItem');
$conn->executeStatement('DELETE FROM GH11163Bucket');
}
public function testFetchEagerModeWithOrderBy(): void
{
// Load entities into database
$this->_em->persist($bucket = new GH11163Bucket(11163));
$this->_em->persist(new GH11163BucketItem(1, $bucket, 2));
$this->_em->persist(new GH11163BucketItem(2, $bucket, 3));
$this->_em->persist(new GH11163BucketItem(3, $bucket, 1));
$this->_em->flush();
$this->_em->clear();
// Fetch entity from database
$dql = 'SELECT bucket FROM ' . GH11163Bucket::class . ' bucket WHERE bucket.id = :id';
$bucket = $this->_em->createQuery($dql)
->setParameter('id', 11163)
->getSingleResult();
// Assert associated entity is loaded eagerly
static::assertInstanceOf(GH11163Bucket::class, $bucket);
static::assertInstanceOf(PersistentCollection::class, $bucket->items);
static::assertTrue($bucket->items->isInitialized());
static::assertCount(3, $bucket->items);
// Assert order of entities
static::assertSame(1, $bucket->items[0]->position);
static::assertSame(3, $bucket->items[0]->id);
static::assertSame(2, $bucket->items[1]->position);
static::assertSame(1, $bucket->items[1]->id);
static::assertSame(3, $bucket->items[2]->position);
static::assertSame(2, $bucket->items[2]->id);
}
}
/**
* @ORM\Entity
*/
class GH11163Bucket
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
*
* @var int
*/
private $id;
/**
* @ORM\OneToMany(
* targetEntity=GH11163BucketItem::class,
* mappedBy="bucket",
* fetch="EAGER"
* )
* @ORM\OrderBy({"position" = "ASC"})
*
* @var Collection<int, GH11163BucketItem>
*/
public $items;
public function __construct(int $id)
{
$this->id = $id;
$this->items = new ArrayCollection();
}
}
/**
* @ORM\Entity
*/
class GH11163BucketItem
{
/**
* @ORM\ManyToOne(targetEntity=GH11163Bucket::class, inversedBy="items")
* @ORM\JoinColumn(nullable=false)
*
* @var GH11163Bucket
*/
private $bucket;
/**
* @ORM\Id
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\Column(type="integer", nullable=false)
*
* @var int
*/
public $position;
public function __construct(int $id, GH11163Bucket $bucket, int $position)
{
$this->id = $id;
$this->bucket = $bucket;
$this->position = $position;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Query\QueryException;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH11487Test extends OrmFunctionalTestCase
{
public function testItThrowsASyntaxErrorOnUnfinishedQuery(): void
{
$this->expectException(QueryException::class);
$this->expectExceptionMessage('Syntax Error');
$this->_em->createQuery('UPDATE Doctrine\Tests\ORM\Functional\Ticket\TaxType t SET t.default =')->execute();
}
}
/** @Entity */
class TaxType
{
/**
* @var int|null
* @Column(type="integer")
* @Id
* @GeneratedValue
*/
public $id;
/**
* @var bool
* @Column(type="boolean")
*/
public $default = false;
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH11500Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH11500AbstractTestEntity::class,
GH11500TestEntityOne::class,
GH11500TestEntityTwo::class,
GH11500TestEntityHolder::class,
]);
}
/**
* @throws ORMException
*/
public function testDeleteOneToManyCollectionWithSingleTableInheritance(): void
{
$testEntityOne = new GH11500TestEntityOne();
$testEntityTwo = new GH11500TestEntityTwo();
$testEntityHolder = new GH11500TestEntityHolder();
$testEntityOne->testEntityHolder = $testEntityHolder;
$testEntityHolder->testEntityOnes->add($testEntityOne);
$testEntityTwo->testEntityHolder = $testEntityHolder;
$testEntityHolder->testEntityTwos->add($testEntityTwo);
$em = $this->getEntityManager();
$em->persist($testEntityOne);
$em->persist($testEntityTwo);
$em->persist($testEntityHolder);
$em->flush();
$testEntityTwosBeforeRemovalOfTestEntityOnes = $testEntityHolder->testEntityTwos->toArray();
$testEntityHolder->testEntityOnes = new ArrayCollection();
$em->persist($testEntityHolder);
$em->flush();
$em->refresh($testEntityHolder);
static::assertEmpty($testEntityHolder->testEntityOnes->toArray(), 'All records should have been deleted');
static::assertEquals($testEntityTwosBeforeRemovalOfTestEntityOnes, $testEntityHolder->testEntityTwos->toArray(), 'Different Entity\'s records should not have been deleted');
}
}
/**
* @ORM\Entity
* @ORM\Table(name="one_to_many_single_table_inheritance_test_entities")
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({"test_entity_one"="GH11500TestEntityOne", "test_entity_two"="GH11500TestEntityTwo"})
*/
class GH11500AbstractTestEntity
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
}
/** @ORM\Entity */
class GH11500TestEntityOne extends GH11500AbstractTestEntity
{
/**
* @ORM\ManyToOne(targetEntity="GH11500TestEntityHolder", inversedBy="testEntityOnes")
* @ORM\JoinColumn(name="test_entity_holder_id", referencedColumnName="id")
*
* @var GH11500TestEntityHolder
*/
public $testEntityHolder;
}
/** @ORM\Entity */
class GH11500TestEntityTwo extends GH11500AbstractTestEntity
{
/**
* @ORM\ManyToOne(targetEntity="GH11500TestEntityHolder", inversedBy="testEntityTwos")
* @ORM\JoinColumn(name="test_entity_holder_id", referencedColumnName="id")
*
* @var GH11500TestEntityHolder
*/
public $testEntityHolder;
}
/** @ORM\Entity */
class GH11500TestEntityHolder
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(targetEntity="GH11500TestEntityOne", mappedBy="testEntityHolder", orphanRemoval=true)
*
* @var Collection
*/
public $testEntityOnes;
/**
* @ORM\OneToMany(targetEntity="GH11500TestEntityTwo", mappedBy="testEntityHolder", orphanRemoval=true)
*
* @var Collection
*/
public $testEntityTwos;
public function __construct()
{
$this->testEntityOnes = new ArrayCollection();
$this->testEntityTwos = new ArrayCollection();
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH6123Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH6123Entity::class
);
}
public function testLoadingRemovedEntityFromDatabaseDoesNotCreateNewManagedEntityInstance(): void
{
$entity = new GH6123Entity();
$this->_em->persist($entity);
$this->_em->flush();
self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($entity));
self::assertFalse($this->_em->getUnitOfWork()->isScheduledForDelete($entity));
$this->_em->remove($entity);
$freshEntity = $this->loadEntityFromDatabase($entity->id);
self::assertSame($entity, $freshEntity);
self::assertSame(UnitOfWork::STATE_REMOVED, $this->_em->getUnitOfWork()->getEntityState($freshEntity));
self::assertTrue($this->_em->getUnitOfWork()->isScheduledForDelete($freshEntity));
}
public function testRemovedEntityCanBePersistedAgain(): void
{
$entity = new GH6123Entity();
$this->_em->persist($entity);
$this->_em->flush();
$this->_em->remove($entity);
self::assertSame(UnitOfWork::STATE_REMOVED, $this->_em->getUnitOfWork()->getEntityState($entity));
self::assertTrue($this->_em->getUnitOfWork()->isScheduledForDelete($entity));
$this->loadEntityFromDatabase($entity->id);
$this->_em->persist($entity);
self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($entity));
self::assertFalse($this->_em->getUnitOfWork()->isScheduledForDelete($entity));
$this->_em->flush();
}
private function loadEntityFromDatabase(int $id): ?GH6123Entity
{
return $this->_em->createQueryBuilder()
->select('e')
->from(GH6123Entity::class, 'e')
->where('e.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
}
/**
* @ORM\Entity
*/
#[ORM\Entity]
class GH6123Entity
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @var int
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
public $id;
}

View File

@@ -12,6 +12,7 @@ use Doctrine\ORM\Events;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\Models\Hydration\SimpleEntity;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\MockObject\MockObject;
@@ -154,4 +155,33 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
$this->expectException(ORMException::class);
$this->hydrator->hydrateAll($this->mockResult, $this->mockResultMapping);
}
public function testToIterableIfYieldAndBreakBeforeFinishAlwaysCleansUp(): void
{
$this->setUpEntitySchema([SimpleEntity::class]);
$entity1 = new SimpleEntity();
$this->_em->persist($entity1);
$entity2 = new SimpleEntity();
$this->_em->persist($entity2);
$this->_em->flush();
$this->_em->clear();
$evm = $this->_em->getEventManager();
$q = $this->_em->createQuery('SELECT e.id FROM ' . SimpleEntity::class . ' e');
// select two entities, but do no iterate
$q->toIterable();
self::assertCount(0, $evm->getListeners(Events::onClear));
// select two entities, but abort after first record
foreach ($q->toIterable() as $result) {
self::assertCount(1, $evm->getListeners(Events::onClear));
break;
}
self::assertCount(0, $evm->getListeners(Events::onClear));
}
}

View File

@@ -64,8 +64,7 @@ class ProxyFactoryTest extends OrmTestCase
public function testReferenceProxyDelegatesLoadingToThePersister(): void
{
$identifier = ['id' => 42];
$proxyClass = 'Proxies\__CG__\Doctrine\Tests\Models\ECommerce\ECommerceFeature';
$persister = $this->getMockBuilderWithOnlyMethods(BasicEntityPersister::class, ['load'])
$persister = $this->getMockBuilderWithOnlyMethods(BasicEntityPersister::class, ['loadById'])
->disableOriginalConstructor()
->getMock();
@@ -75,8 +74,8 @@ class ProxyFactoryTest extends OrmTestCase
$persister
->expects(self::atLeastOnce())
->method('load')
->with(self::equalTo($identifier), self::isInstanceOf($proxyClass))
->method('loadById')
->with(self::equalTo($identifier))
->will(self::returnValue($proxy));
$proxy->getDescription();

View File

@@ -13,10 +13,13 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Internal\Hydration\IterableResult;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\UnitOfWork;
@@ -88,6 +91,33 @@ class QueryTest extends OrmTestCase
self::assertEquals($parameters, $query->getParameters());
}
/**
* @psalm-param LockMode::* $lockMode
*
* @dataProvider provideLockModes
*/
public function testSetLockMode(int $lockMode): void
{
$query = $this->entityManager->wrapInTransaction(static function (EntityManagerInterface $em) use ($lockMode): Query {
$query = $em->createQuery('select u from Doctrine\Tests\Models\CMS\CmsUser u where u.username = ?1');
$query->setLockMode($lockMode);
return $query;
});
self::assertSame($lockMode, $query->getLockMode());
self::assertSame($lockMode, $query->getHint(Query::HINT_LOCK_MODE));
}
/** @psalm-return list<array{LockMode::*}> */
public static function provideLockModes(): array
{
return [
[LockMode::PESSIMISTIC_READ],
[LockMode::PESSIMISTIC_WRITE],
];
}
public function testFree(): void
{
$query = $this->entityManager->createQuery('select u from Doctrine\Tests\Models\CMS\CmsUser u where u.username = ?1');

View File

@@ -24,6 +24,7 @@ use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\ORM\Mapping\OrderBy;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Tools\SchemaValidator;
use Doctrine\Tests\Models\BigIntegers\BigIntegers;
use Doctrine\Tests\Models\ECommerce\ECommerceCart;
use Doctrine\Tests\OrmTestCase;
@@ -240,6 +241,19 @@ class SchemaValidatorTest extends OrmTestCase
$ce
);
}
/**
* @requires PHP 7.4
*/
public function testBigIntProperty(): void
{
$class = $this->em->getClassMetadata(BigIntegers::class);
self::assertSame(
['The field \'Doctrine\Tests\Models\BigIntegers\BigIntegers#three\' has the property type \'float\' that differs from the metadata field type \'int|string\' returned by the \'bigint\' DBAL type.'],
$this->validator->validateClass($class)
);
}
}
/** @MappedSuperclass */

View File

@@ -413,12 +413,18 @@ class UnitOfWorkTest extends OrmTestCase
$entity->id = 123;
$this->_unitOfWork->registerManaged($entity, ['id' => 123], []);
self::assertSame(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($entity));
self::assertFalse($this->_unitOfWork->isScheduledForDelete($entity));
self::assertTrue($this->_unitOfWork->isInIdentityMap($entity));
$this->_unitOfWork->remove($entity);
self::assertFalse($this->_unitOfWork->isInIdentityMap($entity));
self::assertSame(UnitOfWork::STATE_REMOVED, $this->_unitOfWork->getEntityState($entity));
self::assertTrue($this->_unitOfWork->isScheduledForDelete($entity));
self::assertTrue($this->_unitOfWork->isInIdentityMap($entity));
$this->_unitOfWork->persist($entity);
self::assertSame(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($entity));
self::assertFalse($this->_unitOfWork->isScheduledForDelete($entity));
self::assertTrue($this->_unitOfWork->isInIdentityMap($entity));
}

View File

@@ -212,6 +212,10 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
Models\CompositeKeyInheritance\SingleRootClass::class,
Models\CompositeKeyInheritance\SingleChildClass::class,
],
'compositekeyrelations' => [
Models\CompositeKeyRelations\InvoiceClass::class,
Models\CompositeKeyRelations\CustomerClass::class,
],
'taxi' => [
Models\Taxi\PaidRide::class,
Models\Taxi\Ride::class,