mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 06:52:09 +01:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b87781f65e | ||
|
|
c0ff86ef69 | ||
|
|
77b579287c | ||
|
|
27c33cf88d | ||
|
|
6068b61a0d | ||
|
|
00024f7d88 | ||
|
|
255612a1ff | ||
|
|
331f8b52cb | ||
|
|
b2faba62b7 | ||
|
|
da426a0036 | ||
|
|
1891a76f13 | ||
|
|
14bb034fe4 | ||
|
|
afc0aab61a | ||
|
|
e1d7a13a5e | ||
|
|
4262eb495b | ||
|
|
fe6e5a67f8 | ||
|
|
b20a66dcdd | ||
|
|
dc46af27ed | ||
|
|
05ab22710b | ||
|
|
d3b47d2cbb | ||
|
|
026f5bfe1b | ||
|
|
6af7de38e1 | ||
|
|
0b0f2f4d86 | ||
|
|
63d9a898ec | ||
|
|
0bd839a720 | ||
|
|
b65004fc26 | ||
|
|
d2418ab074 | ||
|
|
39a05e31c9 | ||
|
|
ab156a551c | ||
|
|
0fc9208d71 | ||
|
|
fd9e572424 | ||
|
|
76490f2c99 | ||
|
|
f8bbdc40b0 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -15,6 +15,6 @@ phpcs.xml.dist export-ignore
|
||||
phpbench.json export-ignore
|
||||
phpstan.neon export-ignore
|
||||
phpstan-baseline.neon export-ignore
|
||||
phpstan-dbal2.neon export-ignore
|
||||
phpstan-dbal3.neon export-ignore
|
||||
phpstan-params.neon export-ignore
|
||||
phpstan-persistence2.neon export-ignore
|
||||
|
||||
10
.github/workflows/continuous-integration.yml
vendored
10
.github/workflows/continuous-integration.yml
vendored
@@ -150,7 +150,7 @@ jobs:
|
||||
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
uses: "actions/upload-artifact@v7"
|
||||
with:
|
||||
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.stability }}-${{ matrix.native_lazy }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
@@ -265,7 +265,7 @@ jobs:
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
uses: "actions/upload-artifact@v7"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage"
|
||||
path: "coverage.xml"
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
uses: "actions/upload-artifact@v7"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
path: "coverage.xml"
|
||||
@@ -442,7 +442,7 @@ jobs:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 1
|
||||
|
||||
- name: "Upload coverage files"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
uses: "actions/upload-artifact@v7"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
@@ -465,7 +465,7 @@ jobs:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Download coverage files"
|
||||
uses: "actions/download-artifact@v7"
|
||||
uses: "actions/download-artifact@v8"
|
||||
with:
|
||||
path: "reports"
|
||||
|
||||
|
||||
37
UPGRADE.md
37
UPGRADE.md
@@ -27,6 +27,43 @@ At this point, we recommend upgrading to PHP 8.4 first and then directly from
|
||||
ORM 2.19 to 3.5 and up so that you can skip the lazy ghost proxy generation
|
||||
and directly start using native lazy objects.
|
||||
|
||||
# Upgrade to 3.7
|
||||
|
||||
## Conditional breaking changes
|
||||
|
||||
3.7 adds support for `doctrine/collections` 3. If you upgrade to that version
|
||||
of `doctrine/collections`, there are breaking changes in `doctrine/orm` as well,
|
||||
because of cross-package inheritance and type declarations.
|
||||
|
||||
Most notably, `Doctrine\ORM\PersistentCollection::add` no longer returns a boolean:
|
||||
|
||||
```diff
|
||||
- public function add(mixed $value): bool
|
||||
+ public function add(mixed $value): void
|
||||
```
|
||||
|
||||
That method always returned `true`, so you can safely stop using the return
|
||||
value before upgrading.
|
||||
|
||||
Also, if you extend `Doctrine\ORM\Persisters\SqlValueVisitor`, you need to
|
||||
ensure the following methods have a return type in your subclasses:
|
||||
|
||||
- `walkComparison()`
|
||||
- `walkCompositeExpression()`
|
||||
- `walkValue()`
|
||||
|
||||
## Deprecate `EventManager` return type in `EntityManager` methods
|
||||
|
||||
The return type of the following methods has been changed from
|
||||
`Doctrine\Common\EventManager` to `Doctrine\Common\EventManagerInterface`:
|
||||
|
||||
- `Doctrine\ORM\Decorator\EntityManagerDecorator::getEventManager()`
|
||||
- `Doctrine\ORM\EntityManager::getEventManager()`
|
||||
- `Doctrine\ORM\EntityManagerInterface::getEventManager()`
|
||||
|
||||
All three methods continue to return an instance of `EventManager`, however
|
||||
relying on that is deprecated and will no longer be the guaranteed in 4.0.
|
||||
|
||||
# Upgrade to 3.6
|
||||
|
||||
## Deprecate using string expression for default values in mappings
|
||||
|
||||
@@ -34,10 +34,10 @@
|
||||
"php": "^8.1",
|
||||
"ext-ctype": "*",
|
||||
"composer-runtime-api": "^2",
|
||||
"doctrine/collections": "^2.2",
|
||||
"doctrine/collections": "^2.2 || ^3",
|
||||
"doctrine/dbal": "^3.8.2 || ^4",
|
||||
"doctrine/deprecations": "^0.5.3 || ^1",
|
||||
"doctrine/event-manager": "^1.2 || ^2",
|
||||
"doctrine/event-manager": "^2.1.1",
|
||||
"doctrine/inflector": "^1.4 || ^2.0",
|
||||
"doctrine/instantiator": "^1.3 || ^2",
|
||||
"doctrine/lexer": "^3",
|
||||
|
||||
@@ -1,6 +1,41 @@
|
||||
Pagination
|
||||
==========
|
||||
|
||||
Doctrine ORM provides two pagination strategies for DQL queries. Both handle
|
||||
the low-level SQL plumbing, but they make different trade-offs:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Feature
|
||||
- Offset ``Paginator``
|
||||
- ``CursorPaginator``
|
||||
* - Total count
|
||||
- Yes
|
||||
- No
|
||||
* - Random access to page N
|
||||
- Yes
|
||||
- No
|
||||
* - Stable under concurrent inserts/deletes
|
||||
- No
|
||||
- Yes
|
||||
* - Performance on deep pages
|
||||
- Degrades (OFFSET scan)
|
||||
- Constant (index range scan)
|
||||
* - Requires deterministic ORDER BY
|
||||
- No
|
||||
- Yes
|
||||
|
||||
Choose the **Offset Paginator** when you need a total page count or want to
|
||||
let users jump to an arbitrary page number.
|
||||
|
||||
Choose the **Cursor Paginator** when you need stable, high-performance
|
||||
pagination on large datasets and a simple previous/next navigation is
|
||||
sufficient.
|
||||
|
||||
Offset-Based Pagination
|
||||
-----------------------
|
||||
|
||||
Doctrine ORM ships with a Paginator for DQL queries. It
|
||||
has a very simple API and implements the SPL interfaces ``Countable`` and
|
||||
``IteratorAggregate``.
|
||||
@@ -58,3 +93,178 @@ In this way the `DISTINCT` keyword will be omitted and can bring important perfo
|
||||
->setHint(Paginator::HINT_ENABLE_DISTINCT, false)
|
||||
->setFirstResult(0)
|
||||
->setMaxResults(100);
|
||||
|
||||
Cursor-Based Pagination
|
||||
-----------------------
|
||||
|
||||
Doctrine ORM ships with a ``CursorPaginator`` for cursor-based pagination of DQL queries.
|
||||
Unlike offset-based pagination, cursor pagination uses opaque pointers (cursors) derived
|
||||
from the last seen row to fetch the next or previous page. This makes it stable and
|
||||
performant on large datasets — no matter how deep you paginate, the database always uses
|
||||
an index range scan instead of skipping rows.
|
||||
|
||||
.. note::
|
||||
|
||||
Cursor pagination requires a **deterministic ``ORDER BY`` clause**. Every column
|
||||
combination used for sorting must uniquely identify a position in the result set.
|
||||
A common pattern is to sort by a timestamp and then by primary key as a tie-breaker.
|
||||
|
||||
Basic Usage
|
||||
~~~~~~~~~~~
|
||||
|
||||
The ``$cursor`` parameter is an opaque string produced by a previous call to
|
||||
``getNextCursorAsString()`` or ``getPreviousCursorAsString()``. On the first request
|
||||
it is ``null`` or an empty string ``''`` — both are treated identically as the first
|
||||
page. It is typically read from the incoming HTTP query string:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$cursor = $_GET['cursor'] ?? null; // null or '' on the first page
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Tools\CursorPagination\CursorPaginator;
|
||||
|
||||
$dql = 'SELECT p FROM BlogPost p ORDER BY p.createdAt DESC, p.id DESC';
|
||||
$query = $entityManager->createQuery($dql);
|
||||
|
||||
$paginator = (new CursorPaginator($query))
|
||||
->paginate(cursor: $cursor, limit: 15);
|
||||
|
||||
foreach ($paginator as $post) {
|
||||
echo $post->getTitle() . "\n";
|
||||
}
|
||||
|
||||
echo $paginator->getPreviousCursorAsString(); // previous encoded cursor string
|
||||
echo $paginator->getNextCursorAsString(); // next encoded cursor string
|
||||
|
||||
Navigating Pages
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Pass the encoded cursor back on subsequent requests to move forward or backward:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Next page
|
||||
$paginator->paginate(15, $nextCursor);
|
||||
|
||||
// Previous page
|
||||
$paginator->paginate(15, $previousCursor);
|
||||
|
||||
The cursor is an encoded string containing the location at which the next query should begin fetching results, along with the navigation direction.
|
||||
|
||||
API Reference
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
``CursorPaginator::paginate(?string $cursor, int $limit): self``
|
||||
Executes the query and stores the results. Fetches ``$limit + 1`` rows to
|
||||
detect whether a further page exists, then trims the extra row. Returns
|
||||
``$this`` for chaining.
|
||||
|
||||
``CursorPaginator::getNextCursor(): Cursor``
|
||||
Returns the ``Cursor`` object for the next page. Throws a ``LogicException``
|
||||
if there is no next page — call ``hasNextPage()`` first.
|
||||
|
||||
``CursorPaginator::getPreviousCursor(): Cursor``
|
||||
Returns the ``Cursor`` object for the previous page. Throws a ``LogicException``
|
||||
if there is no previous page — call ``hasPreviousPage()`` first.
|
||||
|
||||
``CursorPaginator::getNextCursorAsString(): string``
|
||||
Returns the encoded cursor to retrieve the next page. Throws a
|
||||
``LogicException`` if there is no next page — call ``hasNextPage()`` first.
|
||||
|
||||
``CursorPaginator::getPreviousCursorAsString(): string``
|
||||
Returns the encoded cursor to retrieve the previous page. Throws a
|
||||
``LogicException`` if there is no previous page — call ``hasPreviousPage()`` first.
|
||||
|
||||
``CursorPaginator::hasNextPage(): bool``
|
||||
Returns whether a next page is available.
|
||||
|
||||
``CursorPaginator::hasPreviousPage(): bool``
|
||||
Returns whether a previous page is available.
|
||||
|
||||
``CursorPaginator::hasToPaginate(): bool``
|
||||
Returns whether either a next or previous page exists (i.e. the result
|
||||
set spans more than one page).
|
||||
|
||||
``CursorPaginator::getValues(): array``
|
||||
Returns the raw entity array for the current page.
|
||||
|
||||
``CursorPaginator::getItems(): array``
|
||||
Returns an array of ``CursorItem`` objects, each wrapping an entity and its
|
||||
individual ``Cursor``. Useful when you need per-row cursors.
|
||||
|
||||
``CursorPaginator::getCursorForItem(mixed $item, bool $isNext = true): Cursor``
|
||||
Builds a ``Cursor`` pointing at a specific entity. ``$isNext = true`` means
|
||||
"start *after* this item"; ``false`` means "start *before* this item".
|
||||
|
||||
``CursorPaginator::count(): int``
|
||||
Returns the number of items on the current page (implements ``Countable``).
|
||||
|
||||
**Next page**
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT ...
|
||||
FROM post p
|
||||
WHERE (p.created_at < :cursor_val_0)
|
||||
OR (p.created_at = :cursor_val_0 AND p.id < :cursor_id_1)
|
||||
ORDER BY p.created_at DESC, p.id DESC
|
||||
LIMIT 16 -- limit + 1
|
||||
|
||||
**Previous page**
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT ...
|
||||
FROM post p
|
||||
WHERE (p.created_at > :cursor_val_0)
|
||||
OR (p.created_at = :cursor_val_0 AND p.id > :cursor_id_1)
|
||||
ORDER BY p.created_at ASC, p.id ASC -- reversed
|
||||
LIMIT 16
|
||||
|
||||
HTML Template Example
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The following example shows how to render a paginated list with previous/next
|
||||
navigation links using the ``CursorPaginator`` in a PHP template:
|
||||
|
||||
.. literalinclude:: pagination/cursor-pagination.php
|
||||
:language: php
|
||||
|
||||
Cursor Encoding
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
A cursor is serialized to a URL-safe string via ``Cursor::encodeToString()`` and
|
||||
deserialized back via the static ``Cursor::fromEncodedString()``. The format is a
|
||||
JSON object encoded with URL-safe Base64 (no padding):
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"p.createdAt": "2024-01-15T10:30:00+00:00",
|
||||
"p.id": 42,
|
||||
"_isNext": true
|
||||
}
|
||||
|
||||
The ``_isNext`` flag distinguishes next-page cursors from previous-page cursors.
|
||||
All other keys are the DQL path expressions (``alias.field``) of the ``ORDER BY``
|
||||
columns, and their values are the database representations of the pivot row's
|
||||
field values.
|
||||
|
||||
If you need a different serialization format (e.g. encryption), build it on top of
|
||||
a ``Cursor`` instance: call ``$cursor->toArray()`` to get the raw data, apply your
|
||||
own encoding, and reconstruct with ``new Cursor($parameters, $isNext)``.
|
||||
|
||||
Limitations
|
||||
~~~~~~~~~~~
|
||||
|
||||
- Every ``ORDER BY`` column must map to an entity field. Raw SQL expressions or
|
||||
computed columns in ``ORDER BY`` are not supported.
|
||||
- ``COUNT`` queries are not available; cursor pagination does not know the total
|
||||
number of results by design. If you need a total count, use the
|
||||
offset-based ``Paginator`` described above.
|
||||
- The query must have at least one ``ORDER BY`` item; the paginator throws a
|
||||
``LogicException`` otherwise.
|
||||
|
||||
31
docs/en/tutorials/pagination/cursor-pagination.php
Normal file
31
docs/en/tutorials/pagination/cursor-pagination.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Doctrine\ORM\Tools\CursorPagination\CursorPaginator;
|
||||
|
||||
$cursor = $_GET['cursor'] ?? null;
|
||||
|
||||
$query = $entityManager->createQuery('SELECT p FROM BlogPost p ORDER BY p.createdAt DESC, p.id DESC');
|
||||
|
||||
/** @var CursorPaginator<BlogPost> $paginator */
|
||||
$paginator = (new CursorPaginator($query))
|
||||
->paginate(cursor: $cursor, limit: 15);
|
||||
?>
|
||||
<p><?= $paginator->count() ?> result(s) on this page.</p>
|
||||
|
||||
<ul>
|
||||
<?php foreach ($paginator as $post): ?>
|
||||
<li><?= escape($post->getTitle()) ?></li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
|
||||
<?php if ($paginator->hasToPaginate()): ?>
|
||||
<nav>
|
||||
<?php if ($paginator->hasPreviousPage()): ?>
|
||||
<a href="?cursor=<?= escape($paginator->getPreviousCursorAsString()) ?>">Previous</a>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($paginator->hasNextPage()): ?>
|
||||
<a href="?cursor=<?= escape($paginator->getNextCursorAsString()) ?>">Next</a>
|
||||
<?php endif ?>
|
||||
</nav>
|
||||
<?php endif ?>
|
||||
@@ -52,6 +52,8 @@
|
||||
<rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses">
|
||||
<exclude-pattern>src/Mapping/Driver/LoadMappingFileImplementation.php</exclude-pattern>
|
||||
<exclude-pattern>src/Mapping/GetReflectionClassImplementation.php</exclude-pattern>
|
||||
<exclude-pattern>src/Persisters/SqlValueVisitorImplementation.php</exclude-pattern>
|
||||
<exclude-pattern>src/PersistentCollectionImplementation.php</exclude-pattern>
|
||||
<exclude-pattern>tests/*</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
|
||||
@@ -619,7 +619,7 @@ parameters:
|
||||
path: src/EntityRepository.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\>\.$#'
|
||||
message: '#^Method Doctrine\\ORM\\EntityRepository\:\:matching\(\) should return Doctrine\\Common\\Collections\\AbstractLazyCollection\<int, T of object\> but returns Doctrine\\ORM\\LazyCriteriaCollection\<\(int\|string\), object\>\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/EntityRepository.php
|
||||
@@ -1681,7 +1681,7 @@ parameters:
|
||||
path: src/PersistentCollection.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\PersistentCollection\:\:matching\(\) should return Doctrine\\Common\\Collections\\Collection\<TKey of \(int\|string\), T\> but returns Doctrine\\Common\\Collections\\ReadableCollection\<TKey of \(int\|string\), T\>&Doctrine\\Common\\Collections\\Selectable\<TKey of \(int\|string\), T\>\.$#'
|
||||
message: '#^Method Doctrine\\ORM\\PersistentCollection\:\:matching\(\) should return Doctrine\\Common\\Collections\\Collection\<TKey of \(int\|string\), T\> but returns Doctrine\\Common\\Collections\\ReadableCollection\<TKey of \(int\|string\), T\>\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/PersistentCollection.php
|
||||
@@ -1693,7 +1693,7 @@ parameters:
|
||||
path: src/PersistentCollection.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$callback of function array_walk expects callable\(object, int\)\: mixed, array\{Doctrine\\Common\\Collections\\Collection\<TKey of \(int\|string\), T\>&Doctrine\\Common\\Collections\\Selectable\<TKey of \(int\|string\), T\>, ''add''\} given\.$#'
|
||||
message: '#^Parameter \#2 \$callback of function array_walk expects callable\(object, int\)\: mixed, array\{Doctrine\\Common\\Collections\\Collection\<TKey of \(int\|string\), T\>, ''add''\} given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/PersistentCollection.php
|
||||
@@ -1818,6 +1818,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Persisters/Collection/ManyToManyPersister.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Persisters\\Collection\\ManyToManyPersister\:\:count\(\) should return int\<0, max\> but returns int\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Persisters/Collection/ManyToManyPersister.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Persisters\\Collection\\ManyToManyPersister\:\:delete\(\) has parameter \$collection with generic class Doctrine\\ORM\\PersistentCollection but does not specify its types\: TKey, T$#'
|
||||
identifier: missingType.generics
|
||||
@@ -2580,30 +2586,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Query/Exec/SingleTableDeleteUpdateExecutor.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc type array\<string\> of property Doctrine\\ORM\\Query\\Expr\\Andx\:\:\$allowedClasses is not covariant with PHPDoc type list\<class\-string\> of overridden property Doctrine\\ORM\\Query\\Expr\\Base\:\:\$allowedClasses\.$#'
|
||||
identifier: property.phpDocType
|
||||
count: 1
|
||||
path: src/Query/Expr/Andx.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Query\\Expr\\Func\:\:getArguments\(\) should return list\<mixed\> but returns array\<mixed\>\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Query/Expr/Func.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc type array\<string\> of property Doctrine\\ORM\\Query\\Expr\\Orx\:\:\$allowedClasses is not covariant with PHPDoc type list\<class\-string\> of overridden property Doctrine\\ORM\\Query\\Expr\\Base\:\:\$allowedClasses\.$#'
|
||||
identifier: property.phpDocType
|
||||
count: 1
|
||||
path: src/Query/Expr/Orx.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc type array\<string\> of property Doctrine\\ORM\\Query\\Expr\\Select\:\:\$allowedClasses is not covariant with PHPDoc type list\<class\-string\> of overridden property Doctrine\\ORM\\Query\\Expr\\Base\:\:\$allowedClasses\.$#'
|
||||
identifier: property.phpDocType
|
||||
count: 1
|
||||
path: src/Query/Expr/Select.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Query\\ParameterTypeInferer\:\:inferType\(\) never returns int so it can be removed from the return type\.$#'
|
||||
identifier: return.unusedType
|
||||
@@ -3447,12 +3435,6 @@ parameters:
|
||||
count: 2
|
||||
path: src/UnitOfWork.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#3 \$collection of class Doctrine\\ORM\\PersistentCollection constructor expects Doctrine\\Common\\Collections\\Collection\<\(int\|string\), mixed\>&Doctrine\\Common\\Collections\\Selectable\<\(int\|string\), mixed\>, Doctrine\\Common\\Collections\\Collection given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/UnitOfWork.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#5 \$invoke of method Doctrine\\ORM\\Event\\ListenersInvoker\:\:invoke\(\) expects int\<0, 7\>, int\<min, \-1\>\|int\<1, max\> given\.$#'
|
||||
identifier: argument.type
|
||||
|
||||
@@ -4,6 +4,12 @@ includes:
|
||||
|
||||
parameters:
|
||||
reportUnmatchedIgnoredErrors: false # Some errors in the baseline only apply to DBAL 4
|
||||
excludePaths:
|
||||
# Compatibility shims for Collections 2 vs Collections 3
|
||||
# These have intentional signature mismatches that cannot be resolved
|
||||
- src/PersistentCollectionImplementation.php
|
||||
- src/Persisters/SqlValueVisitorImplementation.php
|
||||
|
||||
ignoreErrors:
|
||||
# Symfony cache supports passing a key prefix to the clear method.
|
||||
- '/^Method Psr\\Cache\\CacheItemPoolInterface\:\:clear\(\) invoked with 1 parameter, 0 required\.$/'
|
||||
@@ -160,3 +166,10 @@ parameters:
|
||||
-
|
||||
message: '~inferType.*never returns~'
|
||||
path: src/Query/ParameterTypeInferer.php
|
||||
|
||||
# Methods used by excluded compatibility shim traits
|
||||
-
|
||||
message: '#^Method .* is unused\.$#'
|
||||
paths:
|
||||
- src/PersistentCollection.php
|
||||
- src/Persisters/SqlValueVisitor.php
|
||||
|
||||
13
phpstan.neon
13
phpstan.neon
@@ -3,6 +3,12 @@ includes:
|
||||
- phpstan-params.neon
|
||||
|
||||
parameters:
|
||||
excludePaths:
|
||||
# Compatibility shims for Collections 2 vs Collections 3
|
||||
# These have intentional signature mismatches that cannot be resolved
|
||||
- src/PersistentCollectionImplementation.php
|
||||
- src/Persisters/SqlValueVisitorImplementation.php
|
||||
|
||||
ignoreErrors:
|
||||
# Symfony cache supports passing a key prefix to the clear method.
|
||||
- '/^Method Psr\\Cache\\CacheItemPoolInterface\:\:clear\(\) invoked with 1 parameter, 0 required\.$/'
|
||||
@@ -54,3 +60,10 @@ parameters:
|
||||
-
|
||||
message: '#Expression on left side of \?\? is not nullable.#'
|
||||
path: src/Mapping/Driver/AttributeDriver.php
|
||||
|
||||
# Methods used by excluded compatibility shim traits
|
||||
-
|
||||
message: '#^Method .* is unused\.$#'
|
||||
paths:
|
||||
- src/PersistentCollection.php
|
||||
- src/Persisters/SqlValueVisitor.php
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Decorator;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\Common\EventManagerInterface;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\Cache;
|
||||
@@ -122,7 +122,7 @@ abstract class EntityManagerDecorator extends ObjectManagerDecorator implements
|
||||
$this->wrapped->refresh($object, $lockMode);
|
||||
}
|
||||
|
||||
public function getEventManager(): EventManager
|
||||
public function getEventManager(): EventManagerInterface
|
||||
{
|
||||
return $this->wrapped->getEventManager();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Doctrine\ORM;
|
||||
use BackedEnum;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\Common\EventManagerInterface;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\Exception\EntityManagerClosed;
|
||||
@@ -511,7 +512,7 @@ class EntityManager implements EntityManagerInterface
|
||||
&& ! $this->unitOfWork->isScheduledForDelete($object);
|
||||
}
|
||||
|
||||
public function getEventManager(): EventManager
|
||||
public function getEventManager(): EventManagerInterface
|
||||
{
|
||||
return $this->eventManager;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\Common\EventManagerInterface;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
@@ -180,9 +180,9 @@ interface EntityManagerInterface extends ObjectManager
|
||||
public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void;
|
||||
|
||||
/**
|
||||
* Gets the EventManager used by the EntityManager.
|
||||
* Gets the EventManagerInterface used by the EntityManager.
|
||||
*/
|
||||
public function getEventManager(): EventManager;
|
||||
public function getEventManager(): EventManagerInterface;
|
||||
|
||||
/**
|
||||
* Gets the Configuration used by the EntityManager.
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Event;
|
||||
|
||||
use Doctrine\Common\EventArgs;
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\Common\EventDispatcher;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\EntityListenerResolver;
|
||||
@@ -23,13 +23,13 @@ class ListenersInvoker
|
||||
/** The Entity listener resolver. */
|
||||
private readonly EntityListenerResolver $resolver;
|
||||
|
||||
/** The EventManager used for dispatching events. */
|
||||
private readonly EventManager $eventManager;
|
||||
/** The EventDispatcher used for dispatching events. */
|
||||
private readonly EventDispatcher $eventDispatcher;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->eventManager = $em->getEventManager();
|
||||
$this->resolver = $em->getConfiguration()->getEntityListenerResolver();
|
||||
$this->eventDispatcher = $em->getEventManager();
|
||||
$this->resolver = $em->getConfiguration()->getEntityListenerResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +52,7 @@ class ListenersInvoker
|
||||
$invoke |= self::INVOKE_LISTENERS;
|
||||
}
|
||||
|
||||
if ($this->eventManager->hasListeners($eventName)) {
|
||||
if ($this->eventDispatcher->hasListeners($eventName)) {
|
||||
$invoke |= self::INVOKE_MANAGER;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class ListenersInvoker
|
||||
}
|
||||
|
||||
if ($invoke & self::INVOKE_MANAGER) {
|
||||
$this->eventManager->dispatchEvent($eventName, $event);
|
||||
$this->eventDispatcher->dispatchEvent($eventName, $event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ use Doctrine\Common\Collections\ReadableCollection;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Persisters\Entity\EntityPersister;
|
||||
|
||||
use function assert;
|
||||
|
||||
/**
|
||||
* A lazy collection that allows a fast count when using criteria object
|
||||
* Once count gets executed once without collection being initialized, result
|
||||
@@ -26,6 +24,7 @@ use function assert;
|
||||
*/
|
||||
class LazyCriteriaCollection extends AbstractLazyCollection implements Selectable
|
||||
{
|
||||
/** @var non-negative-int|null */
|
||||
private int|null $count = null;
|
||||
|
||||
public function __construct(
|
||||
@@ -83,7 +82,6 @@ class LazyCriteriaCollection extends AbstractLazyCollection implements Selectabl
|
||||
public function matching(Criteria $criteria): ReadableCollection&Selectable
|
||||
{
|
||||
$this->initialize();
|
||||
assert($this->collection instanceof Selectable);
|
||||
|
||||
return $this->collection->matching($criteria);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\Common\EventDispatcher;
|
||||
use Doctrine\DBAL\Platforms;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
@@ -55,7 +55,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
private EntityManagerInterface|null $em = null;
|
||||
private AbstractPlatform|null $targetPlatform = null;
|
||||
private MappingDriver|null $driver = null;
|
||||
private EventManager|null $evm = null;
|
||||
private EventDispatcher|null $eventDispatcher = null;
|
||||
|
||||
/** @var mixed[] */
|
||||
private array $embeddablesActiveNesting = [];
|
||||
@@ -109,20 +109,16 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->driver = $this->em->getConfiguration()->getMetadataDriverImpl();
|
||||
$this->evm = $this->em->getEventManager();
|
||||
$this->initialized = true;
|
||||
$this->driver = $this->em->getConfiguration()->getMetadataDriverImpl();
|
||||
$this->eventDispatcher = $this->em->getEventManager();
|
||||
$this->initialized = true;
|
||||
}
|
||||
|
||||
protected function onNotFoundMetadata(string $className): ClassMetadata|null
|
||||
{
|
||||
if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$eventArgs = new OnClassMetadataNotFoundEventArgs($className, $this->em);
|
||||
|
||||
$this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs);
|
||||
$this->eventDispatcher->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs);
|
||||
$classMetadata = $eventArgs->getFoundMetadata();
|
||||
assert($classMetadata instanceof ClassMetadata || $classMetadata === null);
|
||||
|
||||
@@ -245,10 +241,10 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
// During the following event, there may also be updates to the discriminator map as per GH-1257/GH-8402.
|
||||
// So, we must not discover the missing subclasses before that.
|
||||
|
||||
if ($this->evm->hasListeners(Events::loadClassMetadata)) {
|
||||
$eventArgs = new LoadClassMetadataEventArgs($class, $this->em);
|
||||
$this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs);
|
||||
}
|
||||
$this->eventDispatcher->dispatchEvent(
|
||||
Events::loadClassMetadata,
|
||||
new LoadClassMetadataEventArgs($class, $this->em),
|
||||
);
|
||||
|
||||
$this->findAbstractEntityClassesNotListedInDiscriminatorMap($class);
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ final class FieldMapping implements ArrayAccess
|
||||
{
|
||||
$serialized = ['type', 'fieldName', 'columnName'];
|
||||
|
||||
foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted'] as $boolKey) {
|
||||
foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted', 'index'] as $boolKey) {
|
||||
if ($this->$boolKey) {
|
||||
$serialized[] = $boolKey;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ use function strtoupper;
|
||||
*/
|
||||
final class PersistentCollection extends AbstractLazyCollection implements Selectable
|
||||
{
|
||||
use PersistentCollectionImplementation;
|
||||
|
||||
/**
|
||||
* A snapshot of the collection at the moment it was fetched from the database.
|
||||
* This is used to create a diff of the collection at commit time.
|
||||
@@ -402,7 +404,7 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
|
||||
}
|
||||
}
|
||||
|
||||
public function add(mixed $value): bool
|
||||
private function doAdd(mixed $value): void
|
||||
{
|
||||
$this->unwrap()->add($value);
|
||||
|
||||
@@ -411,8 +413,6 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
|
||||
if (is_object($value) && $this->em) {
|
||||
$this->getUnitOfWork()->cancelOrphanRemoval($value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
@@ -504,10 +504,8 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
|
||||
$this->em = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function first()
|
||||
/** {@inheritDoc} */
|
||||
public function first(): mixed
|
||||
{
|
||||
if (! $this->initialized && ! $this->isDirty && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) {
|
||||
$persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping());
|
||||
@@ -618,7 +616,6 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
|
||||
public function unwrap(): Selectable&Collection
|
||||
{
|
||||
assert($this->collection instanceof Collection);
|
||||
assert($this->collection instanceof Selectable);
|
||||
|
||||
return $this->collection;
|
||||
}
|
||||
|
||||
37
src/PersistentCollectionImplementation.php
Normal file
37
src/PersistentCollectionImplementation.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
|
||||
use function defined;
|
||||
|
||||
if (defined(Criteria::class . '::ASC')) {
|
||||
// collections 2
|
||||
/** @internal */
|
||||
trait PersistentCollectionImplementation
|
||||
{
|
||||
abstract private function doAdd(mixed $value): void;
|
||||
|
||||
public function add(mixed $value): bool
|
||||
{
|
||||
$this->doAdd($value);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// collections 3
|
||||
/** @internal */
|
||||
trait PersistentCollectionImplementation
|
||||
{
|
||||
abstract private function doAdd(mixed $value): void;
|
||||
|
||||
public function add(mixed $value): void
|
||||
{
|
||||
$this->doAdd($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ interface CollectionPersister
|
||||
|
||||
/**
|
||||
* Counts the size of this persistent collection.
|
||||
*
|
||||
* @return non-negative-int
|
||||
*/
|
||||
public function count(PersistentCollection $collection): int;
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
|
||||
);
|
||||
}
|
||||
|
||||
/** @return non-negative-int */
|
||||
public function count(PersistentCollection $collection): int
|
||||
{
|
||||
$conditions = [];
|
||||
|
||||
@@ -20,6 +20,7 @@ use function array_reverse;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function implode;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
@@ -86,10 +87,13 @@ class OneToManyPersister extends AbstractCollectionPersister
|
||||
$mapping = $this->getMapping($collection);
|
||||
$persister = $this->uow->getEntityPersister($mapping->targetEntity);
|
||||
|
||||
// Doctrine Collections 2.x support
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
|
||||
// only works with single id identifier entities. Will throw an
|
||||
// exception in Entity Persisters if that is not the case for the
|
||||
// 'mappedBy' field.
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
|
||||
$criteria = $criteria->where(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
|
||||
|
||||
return $persister->count($criteria);
|
||||
}
|
||||
@@ -118,7 +122,8 @@ class OneToManyPersister extends AbstractCollectionPersister
|
||||
// only works with single id identifier entities. Will throw an
|
||||
// exception in Entity Persisters if that is not the case for the
|
||||
// 'mappedBy' field.
|
||||
$criteria = Criteria::create(true);
|
||||
// Doctrine Collections 2.x support
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
|
||||
$criteria->andWhere(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
|
||||
$criteria->andWhere(Criteria::expr()->eq($mapping->indexBy(), $key));
|
||||
@@ -135,10 +140,12 @@ class OneToManyPersister extends AbstractCollectionPersister
|
||||
$mapping = $this->getMapping($collection);
|
||||
$persister = $this->uow->getEntityPersister($mapping->targetEntity);
|
||||
|
||||
// Doctrine Collections 2.x support
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
// only works with single id identifier entities. Will throw an
|
||||
// exception in Entity Persisters if that is not the case for the
|
||||
// 'mappedBy' field.
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
|
||||
$criteria = $criteria->where(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
|
||||
|
||||
return $persister->exists($element, $criteria);
|
||||
}
|
||||
|
||||
@@ -7,25 +7,21 @@ namespace Doctrine\ORM\Persisters;
|
||||
use Doctrine\Common\Collections\Expr\Comparison;
|
||||
use Doctrine\Common\Collections\Expr\CompositeExpression;
|
||||
use Doctrine\Common\Collections\Expr\ExpressionVisitor;
|
||||
use Doctrine\Common\Collections\Expr\Value;
|
||||
|
||||
/**
|
||||
* Extract the values from a criteria/expression
|
||||
*/
|
||||
class SqlValueVisitor extends ExpressionVisitor
|
||||
{
|
||||
use SqlValueVisitorImplementation;
|
||||
|
||||
/** @var mixed[] */
|
||||
private array $values = [];
|
||||
|
||||
/** @var mixed[][] */
|
||||
private array $types = [];
|
||||
|
||||
/**
|
||||
* Converts a comparison expression into the target query language output.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function walkComparison(Comparison $comparison)
|
||||
private function doWalkComparison(Comparison $comparison): mixed
|
||||
{
|
||||
$value = $this->getValueFromComparison($comparison);
|
||||
|
||||
@@ -35,12 +31,7 @@ class SqlValueVisitor extends ExpressionVisitor
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a composite expression into the target query language output.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function walkCompositeExpression(CompositeExpression $expr)
|
||||
private function doWalkCompositeExpression(CompositeExpression $expr): mixed
|
||||
{
|
||||
foreach ($expr->getExpressionList() as $child) {
|
||||
$this->dispatch($child);
|
||||
@@ -49,16 +40,6 @@ class SqlValueVisitor extends ExpressionVisitor
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a value expression into the target query language part.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function walkValue(Value $value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Parameters and Types necessary for matching the last visited expression.
|
||||
*
|
||||
|
||||
86
src/Persisters/SqlValueVisitorImplementation.php
Normal file
86
src/Persisters/SqlValueVisitorImplementation.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Persisters;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Expr\Comparison;
|
||||
use Doctrine\Common\Collections\Expr\CompositeExpression;
|
||||
use Doctrine\Common\Collections\Expr\Value;
|
||||
|
||||
use function defined;
|
||||
|
||||
if (defined(Criteria::class . '::ASC')) {
|
||||
// collections 2
|
||||
/** @internal */
|
||||
trait SqlValueVisitorImplementation
|
||||
{
|
||||
abstract private function doWalkComparison(Comparison $comparison): mixed;
|
||||
|
||||
abstract private function doWalkCompositeExpression(CompositeExpression $comparison): mixed;
|
||||
|
||||
/**
|
||||
* Converts a comparison expression into the target query language output.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @phpstan-ignore missingType.return
|
||||
*/
|
||||
public function walkComparison(Comparison $comparison)
|
||||
{
|
||||
return $this->doWalkComparison($comparison);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a value expression into the target query language part.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @phpstan-ignore missingType.return
|
||||
*/
|
||||
public function walkValue(Value $value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a composite expression into the target query language output.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @phpstan-ignore missingType.return
|
||||
*/
|
||||
public function walkCompositeExpression(CompositeExpression $expr)
|
||||
{
|
||||
return $this->doWalkCompositeExpression($expr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// collections 3
|
||||
/** @internal */
|
||||
trait SqlValueVisitorImplementation
|
||||
{
|
||||
abstract private function doWalkComparison(Comparison $comparison): mixed;
|
||||
|
||||
abstract private function doWalkCompositeExpression(CompositeExpression $comparison): mixed;
|
||||
|
||||
/** Converts a comparison expression into the target query language output. */
|
||||
public function walkComparison(Comparison $comparison): mixed
|
||||
{
|
||||
return $this->doWalkComparison($comparison);
|
||||
}
|
||||
|
||||
/** Converts a value expression into the target query language part. */
|
||||
public function walkValue(Value $value): mixed
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Converts a composite expression into the target query language output. */
|
||||
public function walkCompositeExpression(CompositeExpression $expr): mixed
|
||||
{
|
||||
return $this->doWalkCompositeExpression($expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Expr;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Expression class for building DQL and parts.
|
||||
*
|
||||
@@ -13,7 +15,7 @@ class Andx extends Composite
|
||||
{
|
||||
protected string $separator = ' AND ';
|
||||
|
||||
/** @var string[] */
|
||||
/** @var list<class-string<Stringable>> */
|
||||
protected array $allowedClasses = [
|
||||
Comparison::class,
|
||||
Func::class,
|
||||
|
||||
@@ -13,6 +13,7 @@ use function get_debug_type;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
@@ -27,7 +28,7 @@ abstract class Base implements Stringable
|
||||
protected string $separator = ', ';
|
||||
protected string $postSeparator = ')';
|
||||
|
||||
/** @var list<class-string> */
|
||||
/** @var list<class-string<Stringable>> */
|
||||
protected array $allowedClasses = [];
|
||||
|
||||
/** @var list<string|Stringable> */
|
||||
@@ -58,6 +59,8 @@ abstract class Base implements Stringable
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|Stringable|null $arg
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
@@ -66,7 +69,8 @@ abstract class Base implements Stringable
|
||||
{
|
||||
if ($arg !== null && (! $arg instanceof self || $arg->count() > 0)) {
|
||||
// If we decide to keep Expr\Base instances, we can use this check
|
||||
if (! is_string($arg) && ! in_array($arg::class, $this->allowedClasses, true)) {
|
||||
// @phpstan-ignore function.alreadyNarrowedType (input validation)
|
||||
if (! is_string($arg) && ! (is_object($arg) && in_array($arg::class, $this->allowedClasses, true))) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
"Expression of type '%s' not allowed in this context.",
|
||||
get_debug_type($arg),
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Expr;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Expression class for building DQL OR clauses.
|
||||
*
|
||||
@@ -13,7 +15,7 @@ class Orx extends Composite
|
||||
{
|
||||
protected string $separator = ' OR ';
|
||||
|
||||
/** @var string[] */
|
||||
/** @var list<class-string<Stringable>> */
|
||||
protected array $allowedClasses = [
|
||||
Comparison::class,
|
||||
Func::class,
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Expr;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Expression class for building DQL select statements.
|
||||
*
|
||||
@@ -14,7 +16,7 @@ class Select extends Base
|
||||
protected string $preSeparator = '';
|
||||
protected string $postSeparator = '';
|
||||
|
||||
/** @var string[] */
|
||||
/** @var list<class-string<Stringable>> */
|
||||
protected array $allowedClasses = [Func::class];
|
||||
|
||||
/** @phpstan-var list<string|Func> */
|
||||
|
||||
@@ -62,7 +62,7 @@ EOT);
|
||||
|
||||
ksort($allListeners);
|
||||
} else {
|
||||
$listeners = $eventManager->hasListeners($eventName) ? $eventManager->getListeners($eventName) : [];
|
||||
$listeners = $eventManager->getListeners($eventName);
|
||||
if (! $listeners) {
|
||||
$io->info(sprintf('No listeners are configured for the "%s" event.', $eventName));
|
||||
|
||||
|
||||
100
src/Tools/CursorPagination/Cursor.php
Normal file
100
src/Tools/CursorPagination/Cursor.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\CursorPagination;
|
||||
|
||||
use Doctrine\ORM\Tools\CursorPagination\Exception\InvalidCursor;
|
||||
use JsonException;
|
||||
|
||||
use function base64_decode;
|
||||
use function base64_encode;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
use function rtrim;
|
||||
use function strtr;
|
||||
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
/**
|
||||
* Represents a cursor for cursor-based pagination.
|
||||
*
|
||||
* A cursor contains the parameters needed to fetch the next or previous page of results.
|
||||
*/
|
||||
final class Cursor
|
||||
{
|
||||
/** @param array<string, scalar> $parameters */
|
||||
public function __construct(
|
||||
private readonly array $parameters,
|
||||
private readonly bool $isNext = true,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the cursor is for navigating to the next page.
|
||||
*/
|
||||
public function isNext(): bool
|
||||
{
|
||||
return $this->isNext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the cursor is for navigating to the previous page.
|
||||
*/
|
||||
public function isPrevious(): bool
|
||||
{
|
||||
return ! $this->isNext;
|
||||
}
|
||||
|
||||
/** @return array<string, scalar> */
|
||||
public function toArray(): array
|
||||
{
|
||||
return [...$this->parameters, '_isNext' => $this->isNext];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the cursor to a URL-safe Base64 JSON string.
|
||||
*/
|
||||
public function encodeToString(): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode((string) json_encode($this->toArray())), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a cursor from an encoded string.
|
||||
*
|
||||
* @see CursorWalker::buildCursorCondition() for the security model around cursor manipulation.
|
||||
*
|
||||
* @throws InvalidCursor If decoding fails.
|
||||
*/
|
||||
public static function fromEncodedString(string $encodedString): self
|
||||
{
|
||||
$decoded = base64_decode(strtr($encodedString, '-_', '+/'), strict: true);
|
||||
|
||||
if ($decoded === false) {
|
||||
throw new InvalidCursor($encodedString);
|
||||
}
|
||||
|
||||
try {
|
||||
$parameters = json_decode($decoded, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw new InvalidCursor($encodedString, $e);
|
||||
}
|
||||
|
||||
$isNext = $parameters['_isNext'] ?? true;
|
||||
|
||||
unset($parameters['_isNext']);
|
||||
|
||||
return new self($parameters, $isNext);
|
||||
}
|
||||
}
|
||||
31
src/Tools/CursorPagination/CursorItem.php
Normal file
31
src/Tools/CursorPagination/CursorItem.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\CursorPagination;
|
||||
|
||||
/**
|
||||
* Represents a paginated item with its associated cursor.
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
final class CursorItem
|
||||
{
|
||||
/** @param T $value */
|
||||
public function __construct(
|
||||
private readonly mixed $value,
|
||||
private readonly Cursor $cursor,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return T */
|
||||
public function getValue(): mixed
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getCursor(): Cursor
|
||||
{
|
||||
return $this->cursor;
|
||||
}
|
||||
}
|
||||
21
src/Tools/CursorPagination/CursorOrderByItem.php
Normal file
21
src/Tools/CursorPagination/CursorOrderByItem.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\CursorPagination;
|
||||
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query\AST\PathExpression;
|
||||
|
||||
/** @internal */
|
||||
final class CursorOrderByItem
|
||||
{
|
||||
/** @param ClassMetadata<object>|null $metadata */
|
||||
public function __construct(
|
||||
public readonly PathExpression|string $expression,
|
||||
public readonly OrderDirection $direction,
|
||||
public readonly string $paramKey,
|
||||
public readonly ClassMetadata|null $metadata = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
280
src/Tools/CursorPagination/CursorPaginator.php
Normal file
280
src/Tools/CursorPagination/CursorPaginator.php
Normal file
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\CursorPagination;
|
||||
|
||||
use Countable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\AST\PathExpression;
|
||||
use Doctrine\ORM\Query\QueryException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\ORM\Utility\PersisterHelper;
|
||||
use IteratorAggregate;
|
||||
use LogicException;
|
||||
use Traversable;
|
||||
|
||||
use function array_map;
|
||||
use function array_reverse;
|
||||
|
||||
/**
|
||||
* The cursor paginator handles cursor-based pagination for DQL queries.
|
||||
*
|
||||
* @template T
|
||||
* @implements IteratorAggregate<mixed, T>
|
||||
*/
|
||||
final class CursorPaginator implements IteratorAggregate, Countable
|
||||
{
|
||||
private readonly Query $query;
|
||||
/** @var Collection<int, T>|null */
|
||||
private Collection|null $items = null;
|
||||
|
||||
/** @var list<CursorOrderByItem>|null */
|
||||
private array|null $orderByItems = null;
|
||||
|
||||
private bool $hasMore = false;
|
||||
private Cursor|null $cursor = null;
|
||||
|
||||
public function __construct(Query|QueryBuilder $query)
|
||||
{
|
||||
if ($query instanceof QueryBuilder) {
|
||||
$query = $query->getQuery();
|
||||
}
|
||||
|
||||
$this->query = $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the query.
|
||||
*/
|
||||
public function getQuery(): Query
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginates the query with the given limit and optional cursor.
|
||||
*
|
||||
* @param string|null $cursor The encoded cursor string, null or empty string for the first page.
|
||||
* @param int $limit The maximum number of results to return.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function paginate(string|null $cursor, int $limit): self
|
||||
{
|
||||
$this->cursor = ! empty($cursor) ? Cursor::fromEncodedString($cursor) : null;
|
||||
$shouldReverse = $this->cursor?->isPrevious() ?? false;
|
||||
|
||||
$query = $this->cloneQuery($this->query);
|
||||
|
||||
$this->appendTreeWalker($query);
|
||||
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, $shouldReverse);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, $this->cursor?->getParameters() ?? []);
|
||||
|
||||
$query->setMaxResults($limit + 1);
|
||||
|
||||
$this->items = new ArrayCollection($query->getResult());
|
||||
$this->hasMore = $this->items->count() > $limit;
|
||||
$this->items = new ArrayCollection($this->items->slice(0, $limit));
|
||||
|
||||
$this->orderByItems = $query->getHint(CursorWalker::HINT_CURSOR_ORDER_BY_ITEMS) ?: [];
|
||||
|
||||
if ($this->cursor !== null && $this->cursor->isPrevious()) {
|
||||
$this->items = new ArrayCollection(array_reverse($this->items->toArray(), true));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function cloneQuery(Query $query): Query
|
||||
{
|
||||
$cloneQuery = clone $query;
|
||||
|
||||
$cloneQuery->setParameters(clone $query->getParameters());
|
||||
$cloneQuery->setCacheable(false);
|
||||
|
||||
foreach ($query->getHints() as $name => $value) {
|
||||
$cloneQuery->setHint($name, $value);
|
||||
}
|
||||
|
||||
return $cloneQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a custom tree walker to the tree walkers hint.
|
||||
*/
|
||||
private function appendTreeWalker(Query $query): void
|
||||
{
|
||||
$hints = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
|
||||
|
||||
if ($hints === false) {
|
||||
$hints = [];
|
||||
}
|
||||
|
||||
$hints[] = CursorWalker::class;
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $hints);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return Traversable<mixed, T>
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return $this->items->getIterator();
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return $this->items->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there is a previous page.
|
||||
*/
|
||||
public function hasPreviousPage(): bool
|
||||
{
|
||||
return $this->cursor !== null && ($this->cursor->isNext() || $this->hasMore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there is a next page.
|
||||
*/
|
||||
public function hasNextPage(): bool
|
||||
{
|
||||
return $this->hasMore || ($this->cursor !== null && $this->cursor->isPrevious());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cursor object for the next page.
|
||||
*
|
||||
* @throws LogicException If there is no next page. Check {@see hasNextPage()} first.
|
||||
*/
|
||||
public function getNextCursor(): Cursor
|
||||
{
|
||||
if ($this->items->isEmpty() || ! $this->hasNextPage()) {
|
||||
throw new LogicException('There is no next page. Call hasNextPage() before getNextCursor().');
|
||||
}
|
||||
|
||||
return $this->getCursorForItem($this->items->last());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cursor object for the previous page.
|
||||
*
|
||||
* @throws LogicException If there is no previous page. Check {@see hasPreviousPage()} first.
|
||||
*/
|
||||
public function getPreviousCursor(): Cursor
|
||||
{
|
||||
if ($this->items->isEmpty() || ! $this->hasPreviousPage()) {
|
||||
throw new LogicException('There is no previous page. Call hasPreviousPage() before getPreviousCursor().');
|
||||
}
|
||||
|
||||
return $this->getCursorForItem($this->items->first(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encoded cursor string for the next page.
|
||||
*
|
||||
* @throws LogicException If there is no next page. Check {@see hasNextPage()} first.
|
||||
*/
|
||||
public function getNextCursorAsString(): string
|
||||
{
|
||||
return $this->getNextCursor()->encodeToString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encoded cursor string for the previous page.
|
||||
*
|
||||
* @throws LogicException If there is no previous page. Check {@see hasPreviousPage()} first.
|
||||
*/
|
||||
public function getPreviousCursorAsString(): string
|
||||
{
|
||||
return $this->getPreviousCursor()->encodeToString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cursor for a given item.
|
||||
*
|
||||
* @param mixed $item The item to create a cursor for.
|
||||
* @param bool $isNext Whether the cursor is for the next page.
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws QueryException
|
||||
*/
|
||||
public function getCursorForItem(mixed $item, bool $isNext = true): Cursor
|
||||
{
|
||||
return new Cursor($this->getParametersForItem($item), $isNext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns items wrapped with their associated cursors.
|
||||
*
|
||||
* @return array<int, CursorItem<T>>
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws QueryException
|
||||
*/
|
||||
public function getItems(): array
|
||||
{
|
||||
return array_map(
|
||||
fn (mixed $item) => new CursorItem($item, $this->getCursorForItem($item)),
|
||||
$this->items->toArray(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw entity values.
|
||||
*
|
||||
* @return list<T>
|
||||
*/
|
||||
public function getValues(): array
|
||||
{
|
||||
return $this->items->getValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether pagination is needed.
|
||||
*/
|
||||
public function hasToPaginate(): bool
|
||||
{
|
||||
return $this->hasPreviousPage() || $this->hasNextPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws Query\QueryException
|
||||
* @throws Exception
|
||||
*/
|
||||
private function getParametersForItem(mixed $item): array
|
||||
{
|
||||
$em = $this->query->getEntityManager();
|
||||
$connection = $em->getConnection();
|
||||
$metadata = $em->getMetadataFactory()->hasMetadataFor($item::class)
|
||||
? $em->getClassMetadata($item::class)
|
||||
: null;
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($this->orderByItems as $orderByItem) {
|
||||
if (! $orderByItem->expression instanceof PathExpression) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldName = $orderByItem->expression->field;
|
||||
$orderMetadata = $orderByItem->metadata ?? $metadata;
|
||||
$value = $metadata?->getFieldValue($item, $fieldName) ?? $item->$fieldName;
|
||||
$type = PersisterHelper::getTypeOfField($fieldName, $orderMetadata, $em)[0];
|
||||
|
||||
$result[$orderByItem->paramKey] = $connection->convertToDatabaseValue($value, $type);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
200
src/Tools/CursorPagination/CursorWalker.php
Normal file
200
src/Tools/CursorPagination/CursorWalker.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\CursorPagination;
|
||||
|
||||
use Doctrine\ORM\Query\AST\ComparisonExpression;
|
||||
use Doctrine\ORM\Query\AST\ConditionalExpression;
|
||||
use Doctrine\ORM\Query\AST\ConditionalPrimary;
|
||||
use Doctrine\ORM\Query\AST\ConditionalTerm;
|
||||
use Doctrine\ORM\Query\AST\InputParameter;
|
||||
use Doctrine\ORM\Query\AST\OrderByClause;
|
||||
use Doctrine\ORM\Query\AST\OrderByItem;
|
||||
use Doctrine\ORM\Query\AST\PathExpression;
|
||||
use Doctrine\ORM\Query\AST\SelectStatement;
|
||||
use Doctrine\ORM\Query\AST\WhereClause;
|
||||
use Doctrine\ORM\Query\QueryException;
|
||||
use Doctrine\ORM\Query\TreeWalkerAdapter;
|
||||
use LogicException;
|
||||
|
||||
use function count;
|
||||
use function str_replace;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* TreeWalker for cursor-based pagination.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Extract ORDER BY columns from AST
|
||||
* - Inject WHERE conditions for cursor navigation
|
||||
* - Reverse ORDER BY direction for previous page navigation
|
||||
*/
|
||||
class CursorWalker extends TreeWalkerAdapter
|
||||
{
|
||||
public const HINT_CURSOR_PARAMETERS = 'doctrine.cursor.parameters';
|
||||
public const HINT_CURSOR_REVERSE = 'doctrine.cursor.reverse';
|
||||
public const HINT_CURSOR_ORDER_BY_ITEMS = 'doctrine.cursor.order_by_items';
|
||||
|
||||
public function walkSelectStatement(SelectStatement $selectStatement): void
|
||||
{
|
||||
$query = $this->_getQuery();
|
||||
$shouldReverse = $query->getHint(self::HINT_CURSOR_REVERSE) === true;
|
||||
$cursorParameters = $query->getHint(self::HINT_CURSOR_PARAMETERS);
|
||||
|
||||
if (! isset($selectStatement->orderByClause)) {
|
||||
throw new LogicException('No ORDER BY clause found. Cursor pagination requires a deterministic sort order.');
|
||||
}
|
||||
|
||||
$orderByItems = [];
|
||||
$newOrderByItems = [];
|
||||
|
||||
foreach ($selectStatement->orderByClause->orderByItems as $orderByItem) {
|
||||
$direction = OrderDirection::fromOrderByItem($orderByItem, $shouldReverse);
|
||||
|
||||
$paramKey = $this->getParameterKey($orderByItem->expression);
|
||||
$metadata = $orderByItem->expression instanceof PathExpression
|
||||
? $this->getMetadataForDqlAlias($orderByItem->expression->identificationVariable)
|
||||
: null;
|
||||
|
||||
$orderByItems[] = new CursorOrderByItem($orderByItem->expression, $direction, $paramKey, $metadata);
|
||||
|
||||
$newItem = new OrderByItem($orderByItem->expression);
|
||||
$newItem->type = $direction->value;
|
||||
$newOrderByItems[] = $newItem;
|
||||
}
|
||||
|
||||
$selectStatement->orderByClause = new OrderByClause($newOrderByItems);
|
||||
|
||||
$query->setHint(self::HINT_CURSOR_ORDER_BY_ITEMS, $orderByItems);
|
||||
|
||||
if (empty($cursorParameters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$condition = $this->buildCursorCondition($orderByItems, $cursorParameters);
|
||||
|
||||
if ($condition === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$conditionalPrimary = new ConditionalPrimary();
|
||||
$conditionalPrimary->conditionalExpression = $condition;
|
||||
|
||||
if ($selectStatement->whereClause !== null) {
|
||||
if ($selectStatement->whereClause->conditionalExpression instanceof ConditionalTerm) {
|
||||
$selectStatement->whereClause->conditionalExpression->conditionalFactors[] = $conditionalPrimary;
|
||||
} elseif ($selectStatement->whereClause->conditionalExpression instanceof ConditionalPrimary) {
|
||||
$selectStatement->whereClause->conditionalExpression = new ConditionalExpression(
|
||||
[
|
||||
new ConditionalTerm(
|
||||
[
|
||||
$selectStatement->whereClause->conditionalExpression,
|
||||
$conditionalPrimary,
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
$existingPrimary = new ConditionalPrimary();
|
||||
$existingPrimary->conditionalExpression = $selectStatement->whereClause->conditionalExpression;
|
||||
$selectStatement->whereClause->conditionalExpression = new ConditionalTerm(
|
||||
[
|
||||
$existingPrimary,
|
||||
$conditionalPrimary,
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$selectStatement->whereClause = new WhereClause(
|
||||
new ConditionalExpression(
|
||||
[new ConditionalTerm([$conditionalPrimary])],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively builds a cursor condition:
|
||||
* (col1 > :val1) OR (col1 = :val1 AND col2 > :val2) OR ...
|
||||
*
|
||||
* @param list<CursorOrderByItem> $orderByItems
|
||||
* @param array<string, mixed> $cursorParameters
|
||||
*
|
||||
* @throws QueryException
|
||||
*/
|
||||
private function buildCursorCondition(array $orderByItems, array $cursorParameters, int $index = 0): ConditionalExpression|null
|
||||
{
|
||||
if (! isset($orderByItems[$index])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$orderByItem = $orderByItems[$index];
|
||||
$expression = $orderByItem->expression;
|
||||
$direction = $orderByItem->direction;
|
||||
$paramKey = $orderByItem->paramKey;
|
||||
|
||||
$operator = $direction->operator();
|
||||
|
||||
$paramName = str_replace('.', '_', $paramKey) . '_' . $index;
|
||||
$paramValue = $cursorParameters[$paramKey] ?? null;
|
||||
|
||||
// Security note: $paramKey is derived from the DQL ORDER BY AST, not from the
|
||||
// cursor payload. A tampered cursor can only influence the *values* used as pivot
|
||||
// points, not the columns being filtered. All values are bound via setParameter(),
|
||||
// so SQL injection is not possible. The worst a user can do is navigate to an
|
||||
// arbitrary position in the result set, while remaining bound by the original
|
||||
// query's WHERE constraints.
|
||||
$this->_getQuery()->setParameter($paramName, $paramValue);
|
||||
|
||||
$comparisonExpr = new ComparisonExpression(
|
||||
$expression,
|
||||
$operator,
|
||||
new InputParameter(':' . $paramName),
|
||||
);
|
||||
|
||||
$comparisonPrimary = new ConditionalPrimary();
|
||||
$comparisonPrimary->simpleConditionalExpression = $comparisonExpr;
|
||||
|
||||
if ($index === count($orderByItems) - 1) {
|
||||
return new ConditionalExpression([new ConditionalTerm([$comparisonPrimary])]);
|
||||
}
|
||||
|
||||
$nextCondition = $this->buildCursorCondition($orderByItems, $cursorParameters, $index + 1);
|
||||
|
||||
$equalityExpr = new ComparisonExpression(
|
||||
$expression,
|
||||
'=',
|
||||
new InputParameter(':' . $paramName),
|
||||
);
|
||||
|
||||
$equalityPrimary = new ConditionalPrimary();
|
||||
$equalityPrimary->simpleConditionalExpression = $equalityExpr;
|
||||
|
||||
$nextPrimary = new ConditionalPrimary();
|
||||
$nextPrimary->conditionalExpression = $nextCondition;
|
||||
|
||||
$andPrimary = new ConditionalPrimary();
|
||||
$andPrimary->conditionalExpression = new ConditionalExpression([
|
||||
new ConditionalTerm([$equalityPrimary, $nextPrimary]),
|
||||
]);
|
||||
|
||||
return new ConditionalExpression([
|
||||
new ConditionalTerm([$comparisonPrimary]),
|
||||
new ConditionalTerm([$andPrimary]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameter key for the given expression.
|
||||
*/
|
||||
private function getParameterKey(mixed $expression): string
|
||||
{
|
||||
if ($expression instanceof PathExpression) {
|
||||
return $expression->identificationVariable . '.' . $expression->field;
|
||||
}
|
||||
|
||||
return (string) $expression;
|
||||
}
|
||||
}
|
||||
24
src/Tools/CursorPagination/Exception/InvalidCursor.php
Normal file
24
src/Tools/CursorPagination/Exception/InvalidCursor.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\CursorPagination\Exception;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class InvalidCursor extends InvalidArgumentException
|
||||
{
|
||||
public function __construct(string $cursor, Throwable|null $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
sprintf(
|
||||
'The cursor "%s" could not be decoded.',
|
||||
$cursor,
|
||||
),
|
||||
previous: $previous,
|
||||
);
|
||||
}
|
||||
}
|
||||
37
src/Tools/CursorPagination/OrderDirection.php
Normal file
37
src/Tools/CursorPagination/OrderDirection.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\CursorPagination;
|
||||
|
||||
use Doctrine\ORM\Query\AST\OrderByItem;
|
||||
|
||||
/** @internal */
|
||||
enum OrderDirection: string
|
||||
{
|
||||
case Ascending = 'ASC';
|
||||
case Descending = 'DESC';
|
||||
|
||||
public static function fromOrderByItem(OrderByItem $item, bool $reverse = false): self
|
||||
{
|
||||
$direction = $item->isAsc() ? self::Ascending : self::Descending;
|
||||
|
||||
return $reverse ? $direction->reversed() : $direction;
|
||||
}
|
||||
|
||||
public function operator(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Ascending => '>',
|
||||
self::Descending => '<',
|
||||
};
|
||||
}
|
||||
|
||||
public function reversed(): self
|
||||
{
|
||||
return match ($this) {
|
||||
self::Ascending => self::Descending,
|
||||
self::Descending => self::Ascending,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -389,21 +389,17 @@ class SchemaTool
|
||||
}
|
||||
}
|
||||
|
||||
if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
|
||||
$eventManager->dispatchEvent(
|
||||
ToolEvents::postGenerateSchemaTable,
|
||||
new GenerateSchemaTableEventArgs($class, $schema, $table),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
|
||||
$eventManager->dispatchEvent(
|
||||
ToolEvents::postGenerateSchema,
|
||||
new GenerateSchemaEventArgs($this->em, $schema),
|
||||
ToolEvents::postGenerateSchemaTable,
|
||||
new GenerateSchemaTableEventArgs($class, $schema, $table),
|
||||
);
|
||||
}
|
||||
|
||||
$eventManager->dispatchEvent(
|
||||
ToolEvents::postGenerateSchema,
|
||||
new GenerateSchemaEventArgs($this->em, $schema),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use BackedEnum;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\Common\EventDispatcher;
|
||||
use Doctrine\DBAL;
|
||||
use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
@@ -261,9 +261,9 @@ class UnitOfWork implements PropertyChangedListener
|
||||
private array $collectionPersisters = [];
|
||||
|
||||
/**
|
||||
* The EventManager used for dispatching events.
|
||||
* The EventDispatcher used for dispatching events.
|
||||
*/
|
||||
private readonly EventManager $evm;
|
||||
private readonly EventDispatcher $eventDispatcher;
|
||||
|
||||
/**
|
||||
* The ListenersInvoker used for dispatching events.
|
||||
@@ -314,7 +314,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {
|
||||
$this->evm = $em->getEventManager();
|
||||
$this->eventDispatcher = $em->getEventManager();
|
||||
$this->listenersInvoker = new ListenersInvoker($em);
|
||||
$this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled();
|
||||
$this->identifierFlattener = new IdentifierFlattener($this, $em->getMetadataFactory());
|
||||
@@ -344,10 +344,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$connection->ensureConnectedToPrimary();
|
||||
}
|
||||
|
||||
// Raise preFlush
|
||||
if ($this->evm->hasListeners(Events::preFlush)) {
|
||||
$this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
|
||||
}
|
||||
$this->dispatchPreFlushEvent();
|
||||
|
||||
// Compute changes done since last commit.
|
||||
$this->computeChangeSets();
|
||||
@@ -378,8 +375,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
$this->dispatchOnFlushEvent();
|
||||
|
||||
$conn = $this->em->getConnection();
|
||||
$conn->beginTransaction();
|
||||
$connection->beginTransaction();
|
||||
|
||||
$successful = false;
|
||||
|
||||
@@ -430,7 +426,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
$commitFailed = false;
|
||||
try {
|
||||
if ($conn->commit() === false) {
|
||||
if ($connection->commit() === false) {
|
||||
$commitFailed = true;
|
||||
}
|
||||
} catch (DBAL\Exception $e) {
|
||||
@@ -446,8 +442,8 @@ class UnitOfWork implements PropertyChangedListener
|
||||
if (! $successful) {
|
||||
$this->em->close();
|
||||
|
||||
if ($conn->isTransactionActive()) {
|
||||
$conn->rollBack();
|
||||
if ($connection->isTransactionActive()) {
|
||||
$connection->rollBack();
|
||||
}
|
||||
|
||||
$this->afterTransactionRolledBack();
|
||||
@@ -2299,9 +2295,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$this->eagerLoadingCollections =
|
||||
$this->orphanRemovals = [];
|
||||
|
||||
if ($this->evm->hasListeners(Events::onClear)) {
|
||||
$this->evm->dispatchEvent(Events::onClear, new OnClearEventArgs($this->em));
|
||||
}
|
||||
$this->eventDispatcher->dispatchEvent(Events::onClear, new OnClearEventArgs($this->em));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3147,18 +3141,19 @@ class UnitOfWork implements PropertyChangedListener
|
||||
}
|
||||
}
|
||||
|
||||
private function dispatchPreFlushEvent(): void
|
||||
{
|
||||
$this->eventDispatcher->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
|
||||
}
|
||||
|
||||
private function dispatchOnFlushEvent(): void
|
||||
{
|
||||
if ($this->evm->hasListeners(Events::onFlush)) {
|
||||
$this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
|
||||
}
|
||||
$this->eventDispatcher->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
|
||||
}
|
||||
|
||||
private function dispatchPostFlushEvent(): void
|
||||
{
|
||||
if ($this->evm->hasListeners(Events::postFlush)) {
|
||||
$this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
|
||||
}
|
||||
$this->eventDispatcher->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,8 @@ use Doctrine\Tests\OrmTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
use function defined;
|
||||
|
||||
#[Group('DDC-2183')]
|
||||
abstract class EntityPersisterTestCase extends OrmTestCase
|
||||
{
|
||||
@@ -141,7 +143,7 @@ abstract class EntityPersisterTestCase extends OrmTestCase
|
||||
public function testInvokeExpandCriteriaParameters(): void
|
||||
{
|
||||
$persister = $this->createPersisterDefault();
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
|
||||
$this->entityPersister->expects(self::once())
|
||||
->method('expandCriteriaParameters')
|
||||
@@ -320,7 +322,7 @@ abstract class EntityPersisterTestCase extends OrmTestCase
|
||||
$rsm = new ResultSetMappingBuilder($this->em);
|
||||
$persister = $this->createPersisterDefault();
|
||||
$entity = new Country('Foo');
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
|
||||
$this->em->getUnitOfWork()->registerManaged($entity, ['id' => 1], ['id' => 1, 'name' => 'Foo']);
|
||||
$rsm->addEntityResult(Country::class, 'c');
|
||||
|
||||
@@ -19,6 +19,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function defined;
|
||||
use function get_debug_type;
|
||||
use function sprintf;
|
||||
|
||||
@@ -494,13 +495,13 @@ class ClassTableInheritanceTest extends OrmFunctionalTestCase
|
||||
$this->_em->flush();
|
||||
|
||||
$repository = $this->_em->getRepository(CompanyEmployee::class);
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('department', 'IT'),
|
||||
));
|
||||
self::assertCount(1, $users);
|
||||
|
||||
$repository = $this->_em->getRepository(CompanyManager::class);
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('department', 'IT'),
|
||||
));
|
||||
self::assertCount(1, $users);
|
||||
|
||||
@@ -12,6 +12,8 @@ use Doctrine\Tests\Models\Tweet\Tweet;
|
||||
use Doctrine\Tests\Models\Tweet\User;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
use function defined;
|
||||
|
||||
class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
@@ -66,7 +68,7 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
|
||||
$this->loadFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(DateTimeModel::class);
|
||||
$dates = $repository->matching(Criteria::create(true)->where(
|
||||
$dates = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->lte('datetime', new DateTime('today')),
|
||||
));
|
||||
|
||||
@@ -98,7 +100,7 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
|
||||
$this->loadNullFieldFixtures();
|
||||
$repository = $this->_em->getRepository(DateTimeModel::class);
|
||||
|
||||
$dates = $repository->matching(Criteria::create(true)->where(
|
||||
$dates = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->isNull('time'),
|
||||
));
|
||||
|
||||
@@ -110,7 +112,7 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
|
||||
$this->loadNullFieldFixtures();
|
||||
$repository = $this->_em->getRepository(DateTimeModel::class);
|
||||
|
||||
$dates = $repository->matching(Criteria::create(true)->where(
|
||||
$dates = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('time', null),
|
||||
));
|
||||
|
||||
@@ -122,7 +124,7 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
|
||||
$this->loadNullFieldFixtures();
|
||||
$repository = $this->_em->getRepository(DateTimeModel::class);
|
||||
|
||||
$dates = $repository->matching(Criteria::create(true)->where(
|
||||
$dates = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->neq('time', null),
|
||||
));
|
||||
|
||||
@@ -134,14 +136,14 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
|
||||
$this->loadFixture();
|
||||
$repository = $this->_em->getRepository(DateTimeModel::class);
|
||||
|
||||
$dates = $repository->matching(Criteria::create(true));
|
||||
$dates = $repository->matching(defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create());
|
||||
|
||||
self::assertFalse($dates->isInitialized());
|
||||
self::assertCount(3, $dates);
|
||||
self::assertFalse($dates->isInitialized());
|
||||
|
||||
// Test it can work even with a constraint
|
||||
$dates = $repository->matching(Criteria::create(true)->where(
|
||||
$dates = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->lte('datetime', new DateTime('today')),
|
||||
));
|
||||
|
||||
@@ -169,7 +171,7 @@ class EntityRepositoryCriteriaTest extends OrmFunctionalTestCase
|
||||
|
||||
$this->_em->clear();
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->andWhere($criteria->expr()->contains('content', 'Criteria'));
|
||||
|
||||
$user = $this->_em->find(User::class, $user->id);
|
||||
|
||||
@@ -29,6 +29,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function array_values;
|
||||
use function defined;
|
||||
use function reset;
|
||||
|
||||
class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
@@ -661,7 +662,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$this->loadFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$users = $repository->matching(Criteria::create(true));
|
||||
$users = $repository->matching(defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create());
|
||||
|
||||
self::assertCount(4, $users);
|
||||
}
|
||||
@@ -672,7 +673,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$this->loadFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('username', 'beberlei'),
|
||||
));
|
||||
|
||||
@@ -685,7 +686,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$this->loadFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->neq('username', 'beberlei'),
|
||||
));
|
||||
|
||||
@@ -698,7 +699,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$this->loadFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->in('username', ['beberlei', 'gblanco']),
|
||||
));
|
||||
|
||||
@@ -711,7 +712,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$this->loadFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->notIn('username', ['beberlei', 'gblanco', 'asm89']),
|
||||
));
|
||||
|
||||
@@ -724,7 +725,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$firstUserId = $this->loadFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->lt('id', $firstUserId + 1),
|
||||
));
|
||||
|
||||
@@ -737,7 +738,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$firstUserId = $this->loadFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->lte('id', $firstUserId + 1),
|
||||
));
|
||||
|
||||
@@ -750,7 +751,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$firstUserId = $this->loadFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->gt('id', $firstUserId),
|
||||
));
|
||||
|
||||
@@ -763,7 +764,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$firstUserId = $this->loadFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->gte('id', $firstUserId),
|
||||
));
|
||||
|
||||
@@ -777,7 +778,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
|
||||
$user = $this->_em->find(CmsUser::class, $userId);
|
||||
|
||||
$criteria = Criteria::create(true)->where(
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('user', $user),
|
||||
);
|
||||
|
||||
@@ -798,7 +799,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
|
||||
$user = $this->_em->find(CmsUser::class, $userId);
|
||||
|
||||
$criteria = Criteria::create(true)->where(
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->in('user', [$user]),
|
||||
);
|
||||
|
||||
@@ -818,13 +819,13 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
|
||||
$users = $repository->matching(Criteria::create(true)->where(Criteria::expr()->contains('name', 'Foobar')));
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->contains('name', 'Foobar')));
|
||||
self::assertCount(0, $users);
|
||||
|
||||
$users = $repository->matching(Criteria::create(true)->where(Criteria::expr()->contains('name', 'Rom')));
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->contains('name', 'Rom')));
|
||||
self::assertCount(1, $users);
|
||||
|
||||
$users = $repository->matching(Criteria::create(true)->where(Criteria::expr()->contains('status', 'dev')));
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->contains('status', 'dev')));
|
||||
self::assertCount(2, $users);
|
||||
}
|
||||
|
||||
@@ -834,17 +835,17 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->startsWith('name', 'Foo'),
|
||||
));
|
||||
self::assertCount(0, $users);
|
||||
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->startsWith('name', 'R'),
|
||||
));
|
||||
self::assertCount(1, $users);
|
||||
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->startsWith('status', 'de'),
|
||||
));
|
||||
self::assertCount(2, $users);
|
||||
@@ -856,17 +857,17 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->endsWith('name', 'foo'),
|
||||
));
|
||||
self::assertCount(0, $users);
|
||||
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->endsWith('name', 'oman'),
|
||||
));
|
||||
self::assertCount(1, $users);
|
||||
|
||||
$users = $repository->matching(Criteria::create(true)->where(
|
||||
$users = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->endsWith('status', 'ev'),
|
||||
));
|
||||
self::assertCount(2, $users);
|
||||
@@ -878,8 +879,8 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$fixtures = $this->loadFixtureUserEmail();
|
||||
$user = $this->_em->find(CmsUser::class, $fixtures[0]->id);
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$criteriaIsNull = Criteria::create(true)->where(Criteria::expr()->isNull('email'));
|
||||
$criteriaEqNull = Criteria::create(true)->where(Criteria::expr()->eq('email', null));
|
||||
$criteriaIsNull = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->isNull('email'));
|
||||
$criteriaEqNull = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->eq('email', null));
|
||||
|
||||
$user->setEmail(null);
|
||||
$this->_em->persist($user);
|
||||
@@ -936,7 +937,7 @@ class EntityRepositoryTest extends OrmFunctionalTestCase
|
||||
$this->expectExceptionMessage('Unrecognized field: ');
|
||||
|
||||
$repository = $this->_em->getRepository(CmsUser::class);
|
||||
$result = $repository->matching(Criteria::create(true)->where(
|
||||
$result = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('username = ?; DELETE FROM cms_users; SELECT 1 WHERE 1', 'beberlei'),
|
||||
));
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ use Generator;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
use function class_exists;
|
||||
use function defined;
|
||||
use function sprintf;
|
||||
use function uniqid;
|
||||
|
||||
@@ -559,7 +560,7 @@ EXCEPTION
|
||||
$library = $this->_em->find(Library::class, $library->id);
|
||||
self::assertFalse($library->books->isInitialized(), 'Pre-condition: lazy collection');
|
||||
|
||||
$result = $library->books->matching(Criteria::create(true)->where($comparison));
|
||||
$result = $library->books->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where($comparison));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame($nonfictionBook->id, $result[0]->id);
|
||||
@@ -588,7 +589,7 @@ EXCEPTION
|
||||
$category = $this->_em->find(BookCategory::class, $category->id);
|
||||
self::assertFalse($category->books->isInitialized(), 'Pre-condition: lazy collection');
|
||||
|
||||
$result = $category->books->matching(Criteria::create(true)->where($comparison));
|
||||
$result = $category->books->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where($comparison));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame($nonfictionBook->id, $result[0]->id);
|
||||
|
||||
@@ -17,6 +17,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function assert;
|
||||
use function defined;
|
||||
use function get_class;
|
||||
|
||||
/**
|
||||
@@ -436,7 +437,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
|
||||
|
||||
$user = $this->_em->find($user::class, $user->id);
|
||||
|
||||
$criteria = Criteria::create(true)
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())
|
||||
->orderBy(['name' => Order::Ascending]);
|
||||
|
||||
self::assertEquals(
|
||||
@@ -476,7 +477,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
|
||||
|
||||
$user = $this->_em->find($user::class, $user->id);
|
||||
|
||||
$criteria = Criteria::create(true)
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())
|
||||
->orderBy(['name' => Order::Ascending]);
|
||||
|
||||
self::assertEquals(
|
||||
@@ -499,7 +500,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
|
||||
$groups = $user->groups;
|
||||
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
|
||||
|
||||
$criteria = Criteria::create(true)->setMaxResults(1);
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->setMaxResults(1);
|
||||
$result = $groups->matching($criteria);
|
||||
|
||||
self::assertCount(1, $result);
|
||||
@@ -517,7 +518,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
|
||||
$groups = $user->groups;
|
||||
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
|
||||
|
||||
$criteria = Criteria::create(true)->setFirstResult(1);
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->setFirstResult(1);
|
||||
$result = $groups->matching($criteria);
|
||||
|
||||
self::assertCount(1, $result);
|
||||
@@ -538,7 +539,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
|
||||
$groups = $user->groups;
|
||||
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
|
||||
|
||||
$criteria = Criteria::create(true)->setFirstResult(1)->setMaxResults(3);
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->setFirstResult(1)->setMaxResults(3);
|
||||
$result = $groups->matching($criteria);
|
||||
|
||||
self::assertCount(3, $result);
|
||||
@@ -562,7 +563,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
|
||||
$groups = $user->groups;
|
||||
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
|
||||
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->eq('name', (string) 'Developers_0'));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->eq('name', (string) 'Developers_0'));
|
||||
$result = $groups->matching($criteria);
|
||||
|
||||
self::assertCount(1, $result);
|
||||
@@ -583,7 +584,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
|
||||
$groups = $user->groups;
|
||||
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
|
||||
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->in('name', ['Developers_1']));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->in('name', ['Developers_1']));
|
||||
$result = $groups->matching($criteria);
|
||||
|
||||
self::assertCount(1, $result);
|
||||
@@ -602,7 +603,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
|
||||
$groups = $user->groups;
|
||||
self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection');
|
||||
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->notIn('name', ['Developers_0']));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->notIn('name', ['Developers_0']));
|
||||
$result = $groups->matching($criteria);
|
||||
|
||||
self::assertCount(1, $result);
|
||||
|
||||
@@ -11,6 +11,8 @@ use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function defined;
|
||||
|
||||
/**
|
||||
* Tests a bidirectional one-to-one association mapping (without inheritance).
|
||||
*/
|
||||
@@ -164,14 +166,14 @@ class OneToManyBidirectionalAssociationTest extends OrmFunctionalTestCase
|
||||
$product = $this->_em->find(ECommerceProduct::class, $this->product->getId());
|
||||
$features = $product->getFeatures();
|
||||
|
||||
$results = $features->matching(Criteria::create(true)->where(
|
||||
$results = $features->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('description', 'Model writing tutorial'),
|
||||
));
|
||||
|
||||
self::assertInstanceOf(Collection::class, $results);
|
||||
self::assertCount(1, $results);
|
||||
|
||||
$results = $features->matching(Criteria::create(true));
|
||||
$results = $features->matching(defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create());
|
||||
|
||||
self::assertInstanceOf(Collection::class, $results);
|
||||
self::assertCount(2, $results);
|
||||
@@ -190,7 +192,7 @@ class OneToManyBidirectionalAssociationTest extends OrmFunctionalTestCase
|
||||
$features = $product->getFeatures();
|
||||
$features->add($thirdFeature);
|
||||
|
||||
$results = $features->matching(Criteria::create(true)->where(
|
||||
$results = $features->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('description', 'Model writing tutorial'),
|
||||
));
|
||||
|
||||
@@ -208,14 +210,14 @@ class OneToManyBidirectionalAssociationTest extends OrmFunctionalTestCase
|
||||
$thirdFeature->setDescription('Third feature');
|
||||
$product->addFeature($thirdFeature);
|
||||
|
||||
$results = $features->matching(Criteria::create(true)->where(
|
||||
$results = $features->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('description', 'Third feature'),
|
||||
));
|
||||
|
||||
self::assertInstanceOf(Collection::class, $results);
|
||||
self::assertCount(1, $results);
|
||||
|
||||
$results = $features->matching(Criteria::create(true));
|
||||
$results = $features->matching(defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create());
|
||||
|
||||
self::assertInstanceOf(Collection::class, $results);
|
||||
self::assertCount(3, $results);
|
||||
|
||||
@@ -678,7 +678,7 @@ SQL,
|
||||
|
||||
public function testDifferentResultLengthsDoNotRequireExtraQueryCacheEntries(): void
|
||||
{
|
||||
$dql = 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id >= :id';
|
||||
$dql = 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id >= :id ORDER BY u.id';
|
||||
$query = $this->_em->createQuery($dql);
|
||||
$query->setMaxResults(10);
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ use Doctrine\Tests\Models\ValueConversionType\InversedManyToManyExtraLazyEntity;
|
||||
use Doctrine\Tests\Models\ValueConversionType\OwningManyToManyExtraLazyEntity;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
use function defined;
|
||||
|
||||
class PersistentCollectionCriteriaTest extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
@@ -80,7 +82,7 @@ class PersistentCollectionCriteriaTest extends OrmFunctionalTestCase
|
||||
$repository = $this->_em->getRepository(User::class);
|
||||
|
||||
$user = $repository->findOneBy(['name' => 'ngal']);
|
||||
$tweets = $user->tweets->matching(Criteria::create(true));
|
||||
$tweets = $user->tweets->matching(defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create());
|
||||
|
||||
self::assertInstanceOf(LazyCriteriaCollection::class, $tweets);
|
||||
self::assertFalse($tweets->isInitialized());
|
||||
@@ -88,7 +90,7 @@ class PersistentCollectionCriteriaTest extends OrmFunctionalTestCase
|
||||
self::assertFalse($tweets->isInitialized());
|
||||
|
||||
// Make sure it works with constraints
|
||||
$tweets = $user->tweets->matching(Criteria::create(true)->where(
|
||||
$tweets = $user->tweets->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('content', 'Foo'),
|
||||
));
|
||||
|
||||
@@ -117,7 +119,7 @@ class PersistentCollectionCriteriaTest extends OrmFunctionalTestCase
|
||||
|
||||
$parent = $this->_em->find(OwningManyToManyExtraLazyEntity::class, $parent->id2);
|
||||
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->eq('id1', 'Bob'));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->eq('id1', 'Bob'));
|
||||
|
||||
$result = $parent->associatedEntities->matching($criteria);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function class_exists;
|
||||
use function defined;
|
||||
|
||||
class PersistentCollectionTest extends OrmFunctionalTestCase
|
||||
{
|
||||
@@ -89,7 +90,7 @@ class PersistentCollectionTest extends OrmFunctionalTestCase
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
|
||||
$collectionHolder = $this->_em->find(PersistentCollectionHolder::class, $collectionHolder->getId());
|
||||
$collectionHolder->getCollection()->matching($criteria);
|
||||
|
||||
@@ -10,6 +10,8 @@ use Doctrine\Tests\Models\Cache\Country;
|
||||
use Doctrine\Tests\Models\Cache\State;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function defined;
|
||||
|
||||
#[Group('DDC-2183')]
|
||||
class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
|
||||
{
|
||||
@@ -26,7 +28,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
|
||||
$repository = $this->_em->getRepository(Country::class);
|
||||
$this->getQueryLog()->reset()->enable();
|
||||
$name = $this->countries[0]->getName();
|
||||
$result1 = $repository->matching(Criteria::create(true)->where(
|
||||
$result1 = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('name', $name),
|
||||
));
|
||||
|
||||
@@ -41,7 +43,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
|
||||
|
||||
$this->_em->clear();
|
||||
|
||||
$result2 = $repository->matching(Criteria::create(true)->where(
|
||||
$result2 = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('name', $name),
|
||||
));
|
||||
|
||||
@@ -65,7 +67,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
|
||||
|
||||
$repository = $this->_em->getRepository(Country::class);
|
||||
$this->getQueryLog()->reset()->enable();
|
||||
$result1 = $repository->matching(Criteria::create(true)->where(
|
||||
$result1 = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('name', $this->countries[0]->getName()),
|
||||
));
|
||||
|
||||
@@ -79,7 +81,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
|
||||
|
||||
$this->_em->clear();
|
||||
|
||||
$result2 = $repository->matching(Criteria::create(true)->where(
|
||||
$result2 = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('name', $this->countries[0]->getName()),
|
||||
));
|
||||
|
||||
@@ -94,7 +96,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
|
||||
self::assertEquals($this->countries[0]->getId(), $result2[0]->getId());
|
||||
self::assertEquals($this->countries[0]->getName(), $result2[0]->getName());
|
||||
|
||||
$result3 = $repository->matching(Criteria::create(true)->where(
|
||||
$result3 = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('name', $this->countries[1]->getName()),
|
||||
));
|
||||
|
||||
@@ -109,7 +111,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
|
||||
self::assertEquals($this->countries[1]->getId(), $result3[0]->getId());
|
||||
self::assertEquals($this->countries[1]->getName(), $result3[0]->getName());
|
||||
|
||||
$result4 = $repository->matching(Criteria::create(true)->where(
|
||||
$result4 = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('name', $this->countries[1]->getName()),
|
||||
));
|
||||
|
||||
@@ -134,7 +136,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
|
||||
$itemName = $this->states[0]->getCities()->get(0)->getName();
|
||||
$this->getQueryLog()->reset()->enable();
|
||||
$collection = $entity->getCities();
|
||||
$matching = $collection->matching(Criteria::create(true)->where(
|
||||
$matching = $collection->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('name', $itemName),
|
||||
));
|
||||
|
||||
@@ -147,7 +149,7 @@ class SecondLevelCacheCriteriaTest extends SecondLevelCacheFunctionalTestCase
|
||||
$entity = $this->_em->find(State::class, $this->states[0]->getId());
|
||||
$this->getQueryLog()->reset()->enable();
|
||||
$collection = $entity->getCities();
|
||||
$matching = $collection->matching(Criteria::create(true)->where(
|
||||
$matching = $collection->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('name', $itemName),
|
||||
));
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function array_map;
|
||||
use function defined;
|
||||
use function sort;
|
||||
|
||||
class SingleTableInheritanceTest extends OrmFunctionalTestCase
|
||||
@@ -354,13 +355,13 @@ class SingleTableInheritanceTest extends OrmFunctionalTestCase
|
||||
$this->loadFullFixture();
|
||||
|
||||
$repository = $this->_em->getRepository(CompanyContract::class);
|
||||
$contracts = $repository->matching(Criteria::create(true)->where(
|
||||
$contracts = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('salesPerson', $this->salesPerson),
|
||||
));
|
||||
self::assertCount(3, $contracts);
|
||||
|
||||
$repository = $this->_em->getRepository(CompanyFixContract::class);
|
||||
$contracts = $repository->matching(Criteria::create(true)->where(
|
||||
$contracts = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('salesPerson', $this->salesPerson),
|
||||
));
|
||||
self::assertCount(1, $contracts);
|
||||
@@ -376,7 +377,7 @@ class SingleTableInheritanceTest extends OrmFunctionalTestCase
|
||||
$this->expectException(MatchingAssociationFieldRequiresObject::class);
|
||||
$this->expectExceptionMessage('annot match on Doctrine\Tests\Models\Company\CompanyContract::salesPerson with a non-object value.');
|
||||
|
||||
$contracts = $repository->matching(Criteria::create(true)->where(
|
||||
$contracts = $repository->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->eq('salesPerson', $this->salesPerson->getId()),
|
||||
));
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class DDC1452Test extends OrmFunctionalTestCase
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$dql = 'SELECT a, b, ba FROM ' . __NAMESPACE__ . '\DDC1452EntityA AS a LEFT JOIN a.entitiesB AS b LEFT JOIN b.entityATo AS ba';
|
||||
$dql = 'SELECT a, b, ba FROM ' . __NAMESPACE__ . '\DDC1452EntityA AS a LEFT JOIN a.entitiesB AS b LEFT JOIN b.entityATo AS ba ORDER BY a.id';
|
||||
$results = $this->_em->createQuery($dql)->setMaxResults(1)->getResult();
|
||||
|
||||
self::assertSame($results[0], $results[0]->entitiesB[0]->entityAFrom);
|
||||
|
||||
@@ -16,6 +16,8 @@ use Doctrine\ORM\Mapping\OneToMany;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function defined;
|
||||
|
||||
#[Group('DDC-2106')]
|
||||
class DDC2106Test extends OrmFunctionalTestCase
|
||||
{
|
||||
@@ -39,7 +41,7 @@ class DDC2106Test extends OrmFunctionalTestCase
|
||||
$entityWithoutId = new DDC2106Entity();
|
||||
$this->_em->persist($entityWithoutId);
|
||||
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->eq('parent', $entityWithoutId));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->eq('parent', $entityWithoutId));
|
||||
|
||||
self::assertCount(0, $entity->children->matching($criteria));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ use Doctrine\Tests\Models\Company\CompanyManager;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function defined;
|
||||
|
||||
class DDC3719Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
@@ -44,7 +46,7 @@ class DDC3719Test extends OrmFunctionalTestCase
|
||||
$contracts = $manager->managedContracts;
|
||||
self::assertCount(2, $contracts);
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where(Criteria::expr()->eq('completed', true));
|
||||
|
||||
$completedContracts = $contracts->matching($criteria);
|
||||
|
||||
@@ -10,6 +10,8 @@ use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function defined;
|
||||
|
||||
final class GH6740Test extends OrmFunctionalTestCase
|
||||
{
|
||||
private int $productId;
|
||||
@@ -49,7 +51,7 @@ final class GH6740Test extends OrmFunctionalTestCase
|
||||
public function testCollectionFilteringLteOperator(): void
|
||||
{
|
||||
$product = $this->_em->find(ECommerceProduct::class, $this->productId);
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->lte('id', $this->secondCategoryId));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->lte('id', $this->secondCategoryId));
|
||||
|
||||
self::assertCount(2, $product->getCategories()->matching($criteria));
|
||||
}
|
||||
@@ -58,7 +60,7 @@ final class GH6740Test extends OrmFunctionalTestCase
|
||||
public function testCollectionFilteringLtOperator(): void
|
||||
{
|
||||
$product = $this->_em->find(ECommerceProduct::class, $this->productId);
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->lt('id', $this->secondCategoryId));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->lt('id', $this->secondCategoryId));
|
||||
|
||||
self::assertCount(1, $product->getCategories()->matching($criteria));
|
||||
}
|
||||
@@ -67,7 +69,7 @@ final class GH6740Test extends OrmFunctionalTestCase
|
||||
public function testCollectionFilteringGteOperator(): void
|
||||
{
|
||||
$product = $this->_em->find(ECommerceProduct::class, $this->productId);
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->gte('id', $this->firstCategoryId));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->gte('id', $this->firstCategoryId));
|
||||
|
||||
self::assertCount(2, $product->getCategories()->matching($criteria));
|
||||
}
|
||||
@@ -76,7 +78,7 @@ final class GH6740Test extends OrmFunctionalTestCase
|
||||
public function testCollectionFilteringGtOperator(): void
|
||||
{
|
||||
$product = $this->_em->find(ECommerceProduct::class, $this->productId);
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->gt('id', $this->firstCategoryId));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->gt('id', $this->firstCategoryId));
|
||||
|
||||
self::assertCount(1, $product->getCategories()->matching($criteria));
|
||||
}
|
||||
@@ -85,7 +87,7 @@ final class GH6740Test extends OrmFunctionalTestCase
|
||||
public function testCollectionFilteringEqualsOperator(): void
|
||||
{
|
||||
$product = $this->_em->find(ECommerceProduct::class, $this->productId);
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->eq('id', $this->firstCategoryId));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->eq('id', $this->firstCategoryId));
|
||||
|
||||
self::assertCount(1, $product->getCategories()->matching($criteria));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ use Doctrine\Tests\Models\GH7717\GH7717Child;
|
||||
use Doctrine\Tests\Models\GH7717\GH7717Parent;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
use function defined;
|
||||
|
||||
final class GH7717Test extends OrmFunctionalTestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
@@ -37,7 +39,7 @@ final class GH7717Test extends OrmFunctionalTestCase
|
||||
|
||||
$parent = $this->_em->find(GH7717Parent::class, 1);
|
||||
|
||||
$this->assertCount(1, $parent->children->matching(Criteria::create(true)->where(
|
||||
$this->assertCount(1, $parent->children->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(
|
||||
Criteria::expr()->isNull('nullableProperty'),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
|
||||
use function defined;
|
||||
|
||||
#[Group('GH7737')]
|
||||
class GH7737Test extends OrmFunctionalTestCase
|
||||
{
|
||||
@@ -43,7 +45,7 @@ class GH7737Test extends OrmFunctionalTestCase
|
||||
$query = $this->_em->createQueryBuilder()
|
||||
->select('person')
|
||||
->from(GH7737Person::class, 'person')
|
||||
->addCriteria(Criteria::create(true)->where(Criteria::expr()->memberOf(':group', 'person.groups')))
|
||||
->addCriteria((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->memberOf(':group', 'person.groups')))
|
||||
->getQuery();
|
||||
|
||||
$group1 = $this->_em->find(GH7737Group::class, 1);
|
||||
|
||||
@@ -20,6 +20,7 @@ use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function defined;
|
||||
|
||||
#[Group('GH7767')]
|
||||
class GH7767Test extends OrmFunctionalTestCase
|
||||
@@ -45,7 +46,7 @@ class GH7767Test extends OrmFunctionalTestCase
|
||||
$parent = $this->_em->find(GH7767ParentEntity::class, 1);
|
||||
assert($parent instanceof GH7767ParentEntity);
|
||||
|
||||
$children = $parent->getChildren()->matching(Criteria::create(true));
|
||||
$children = $parent->getChildren()->matching(defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create());
|
||||
|
||||
self::assertEquals(100, $children[0]->position);
|
||||
self::assertEquals(200, $children[1]->position);
|
||||
@@ -58,7 +59,7 @@ class GH7767Test extends OrmFunctionalTestCase
|
||||
assert($parent instanceof GH7767ParentEntity);
|
||||
|
||||
$children = $parent->getChildren()->matching(
|
||||
Criteria::create(true)->orderBy(['position' => class_exists(Order::class) ? Order::Descending : 'DESC']),
|
||||
(defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->orderBy(['position' => class_exists(Order::class) ? Order::Descending : 'DESC']),
|
||||
);
|
||||
|
||||
self::assertEquals(300, $children[0]->position);
|
||||
|
||||
@@ -20,6 +20,7 @@ use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function defined;
|
||||
|
||||
#[Group('GH7836')]
|
||||
class GH7836Test extends OrmFunctionalTestCase
|
||||
@@ -45,7 +46,7 @@ class GH7836Test extends OrmFunctionalTestCase
|
||||
$parent = $this->_em->find(GH7836ParentEntity::class, 1);
|
||||
assert($parent instanceof GH7836ParentEntity);
|
||||
|
||||
$children = $parent->getChildren()->matching(Criteria::create(true));
|
||||
$children = $parent->getChildren()->matching(defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create());
|
||||
|
||||
self::assertSame(100, $children[0]->position);
|
||||
self::assertSame('bar', $children[0]->name);
|
||||
@@ -61,7 +62,7 @@ class GH7836Test extends OrmFunctionalTestCase
|
||||
assert($parent instanceof GH7836ParentEntity);
|
||||
|
||||
$children = $parent->getChildren()->matching(
|
||||
Criteria::create(true)->orderBy(
|
||||
(defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->orderBy(
|
||||
class_exists(Order::class)
|
||||
? ['position' => Order::Descending, 'name' => Order::Ascending]
|
||||
: ['position' => 'DESC', 'name' => 'ASC'],
|
||||
@@ -82,7 +83,7 @@ class GH7836Test extends OrmFunctionalTestCase
|
||||
assert($parent instanceof GH7836ParentEntity);
|
||||
|
||||
$children = $parent->getChildren()->matching(
|
||||
Criteria::create(true)->orderBy(
|
||||
(defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->orderBy(
|
||||
class_exists(Order::class)
|
||||
? ['name' => Order::Ascending, 'position' => Order::Ascending]
|
||||
: ['name' => 'ASC', 'position' => 'ASC'],
|
||||
|
||||
@@ -15,6 +15,8 @@ use Doctrine\ORM\Mapping\ManyToMany;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function defined;
|
||||
|
||||
#[Group('GH-9109')]
|
||||
class GH9109Test extends OrmFunctionalTestCase
|
||||
{
|
||||
@@ -67,7 +69,7 @@ class GH9109Test extends OrmFunctionalTestCase
|
||||
self::assertEquals($userLastName, $user->getLastName());
|
||||
|
||||
// assert NOT QUOTED will WORK with Criteria
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->eq('lastName', $userLastName));
|
||||
$user = $persistedProduct->getBuyers()->matching($criteria)->first();
|
||||
self::assertInstanceOf(GH9109User::class, $user);
|
||||
@@ -79,7 +81,7 @@ class GH9109Test extends OrmFunctionalTestCase
|
||||
self::assertEquals($userFirstName, $user->getFirstName());
|
||||
|
||||
// assert QUOTED will WORK with Criteria
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->eq('firstName', $userFirstName));
|
||||
$user = $persistedProduct->getBuyers()->matching($criteria)->first();
|
||||
self::assertInstanceOf(GH9109User::class, $user);
|
||||
|
||||
@@ -12,6 +12,8 @@ use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use Generator;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
use function defined;
|
||||
|
||||
class ManyToManyCriteriaMatchingTest extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
@@ -48,7 +50,7 @@ class ManyToManyCriteriaMatchingTest extends OrmFunctionalTestCase
|
||||
$associated = $this->_em->find(InversedManyToManyEntity::class, 'associated');
|
||||
self::assertFalse($associated->associatedEntities->isInitialized(), 'Pre-condition: lazy collection');
|
||||
|
||||
$result = $associated->associatedEntities->matching(Criteria::create(true)->where($comparison));
|
||||
$result = $associated->associatedEntities->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where($comparison));
|
||||
|
||||
$l = $this->getQueryLog();
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use Generator;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
use function defined;
|
||||
|
||||
class OneToManyCriteriaMatchingTest extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
@@ -48,7 +50,7 @@ class OneToManyCriteriaMatchingTest extends OrmFunctionalTestCase
|
||||
$entityWithCollection = $this->_em->find(InversedOneToManyEntity::class, 'associated');
|
||||
self::assertFalse($entityWithCollection->associatedEntities->isInitialized(), 'Pre-condition: lazy collection');
|
||||
|
||||
$result = $entityWithCollection->associatedEntities->matching(Criteria::create(true)->where($comparison));
|
||||
$result = $entityWithCollection->associatedEntities->matching((defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where($comparison));
|
||||
|
||||
$l = $this->getQueryLog();
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
use function defined;
|
||||
|
||||
#[CoversClass(LazyCriteriaCollection::class)]
|
||||
class LazyCriteriaCollectionTest extends TestCase
|
||||
{
|
||||
@@ -22,7 +24,7 @@ class LazyCriteriaCollectionTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->persister = $this->createMock(EntityPersister::class);
|
||||
$this->criteria = Criteria::create(true);
|
||||
$this->criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$this->lazyCriteriaCollection = new LazyCriteriaCollection($this->persister, $this->criteria);
|
||||
}
|
||||
|
||||
@@ -78,7 +80,7 @@ class LazyCriteriaCollectionTest extends TestCase
|
||||
->with($this->criteria)
|
||||
->willReturn([$foo, $bar, $baz]);
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
|
||||
$criteria->andWhere($criteria->expr()->eq('val', 'foo'));
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ final class FieldMappingTest extends TestCase
|
||||
$mapping->precision = 10;
|
||||
$mapping->scale = 2;
|
||||
$mapping->unique = true;
|
||||
$mapping->index = true;
|
||||
$mapping->inherited = self::class;
|
||||
$mapping->originalClass = self::class;
|
||||
$mapping->originalField = 'id';
|
||||
@@ -57,6 +58,7 @@ final class FieldMappingTest extends TestCase
|
||||
self::assertSame(10, $resurrectedMapping->precision);
|
||||
self::assertSame(2, $resurrectedMapping->scale);
|
||||
self::assertTrue($resurrectedMapping->unique);
|
||||
self::assertTrue($resurrectedMapping->index);
|
||||
self::assertSame(self::class, $resurrectedMapping->inherited);
|
||||
self::assertSame(self::class, $resurrectedMapping->originalClass);
|
||||
self::assertSame('id', $resurrectedMapping->originalField);
|
||||
|
||||
@@ -12,6 +12,8 @@ use Doctrine\Tests\Models\GeoNames\Admin1AlternateName;
|
||||
use Doctrine\Tests\Models\GeoNames\Country;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
|
||||
use function defined;
|
||||
|
||||
class BasicEntityPersisterCompositeTypeParametersTest extends OrmTestCase
|
||||
{
|
||||
protected BasicEntityPersister $persister;
|
||||
@@ -46,7 +48,7 @@ class BasicEntityPersisterCompositeTypeParametersTest extends OrmTestCase
|
||||
$country = new Country('IT', 'Italy');
|
||||
$admin1 = new Admin1(10, 'Rome', $country);
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->andWhere(Criteria::expr()->eq('admin1', $admin1));
|
||||
|
||||
[$values, $types] = $this->persister->expandCriteriaParameters($criteria);
|
||||
|
||||
@@ -24,6 +24,7 @@ use PHPUnit\Framework\Attributes\Group;
|
||||
use ReflectionMethod;
|
||||
|
||||
use function array_slice;
|
||||
use function defined;
|
||||
use function enum_exists;
|
||||
|
||||
class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase
|
||||
@@ -185,7 +186,7 @@ class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase
|
||||
self::assertEquals('SELECT COUNT(*) FROM "not-a-simple-entity" t0 WHERE t0."simple-entity-value" = ?', $statement);
|
||||
|
||||
// Using a criteria object
|
||||
$criteria = Criteria::create(true)->where(Criteria::expr()->eq('value', 'bar'));
|
||||
$criteria = (defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create())->where(Criteria::expr()->eq('value', 'bar'));
|
||||
$statement = $persister->getCountSQL($criteria);
|
||||
self::assertEquals('SELECT COUNT(*) FROM "not-a-simple-entity" t0 WHERE t0."simple-entity-value" = ?', $statement);
|
||||
}
|
||||
|
||||
@@ -374,11 +374,27 @@ class ExprTest extends OrmTestCase
|
||||
|
||||
public function testAddThrowsException(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$orExpr = $this->expr->orX();
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$orExpr->add($this->expr->quot(5, 2));
|
||||
}
|
||||
|
||||
#[DataProvider('provideInvalidTypesForAdd')]
|
||||
public function testAddThrowsExceptionOnInvalidType(mixed $arg): void
|
||||
{
|
||||
$orExpr = $this->expr->orX();
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$orExpr->add($arg);
|
||||
}
|
||||
|
||||
/** @return Generator<string, array{mixed}> */
|
||||
public static function provideInvalidTypesForAdd(): Generator
|
||||
{
|
||||
yield 'integer 1' => [1];
|
||||
yield 'object' => [(object) ['foo' => 'bar']];
|
||||
yield 'array' => [['foo' => 'bar']];
|
||||
}
|
||||
|
||||
#[Group('DDC-1683')]
|
||||
public function testBooleanLiteral(): void
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
use function array_filter;
|
||||
use function defined;
|
||||
|
||||
/**
|
||||
* Test case for the QueryBuilder class used to build DQL query string in a
|
||||
@@ -513,7 +514,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb->select('u')
|
||||
->from(CmsUser::class, 'u');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->eq('field', 'value'));
|
||||
|
||||
$qb->addCriteria($criteria);
|
||||
@@ -527,7 +528,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('alias1')->from(CmsUser::class, 'alias1');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->andX(
|
||||
$criteria->expr()->eq('field', 'value1'),
|
||||
$criteria->expr()->eq('field', 'value2'),
|
||||
@@ -546,7 +547,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('alias1')->from(CmsUser::class, 'alias1');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->eq('field', 'value1'));
|
||||
$criteria->andWhere($criteria->expr()->gt('field', 'value2'));
|
||||
|
||||
@@ -563,7 +564,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('alias1')->from(CmsUser::class, 'alias1');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->eq('field1', 'value1'));
|
||||
$criteria->andWhere($criteria->expr()->gt('field2', 'value2'));
|
||||
|
||||
@@ -580,7 +581,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('alias1')->from(CmsUser::class, 'alias1');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->eq('field1', 'value1'));
|
||||
$criteria->andWhere($criteria->expr()->gt('field2', 'value2'));
|
||||
|
||||
@@ -597,7 +598,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('alias1')->from(CmsUser::class, 'alias1');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->eq('field1', 'value1'));
|
||||
$criteria->andWhere($criteria->expr()->gt('field1', 'value2'));
|
||||
|
||||
@@ -614,7 +615,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb->select('u')
|
||||
->from(CmsUser::class, 'u');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->orderBy(['field' => Order::Descending]);
|
||||
|
||||
$qb->addCriteria($criteria);
|
||||
@@ -631,7 +632,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
->from(CmsUser::class, 'u')
|
||||
->join('u.article', 'a');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->orderBy(['a.field' => Order::Descending]);
|
||||
|
||||
$qb->addCriteria($criteria);
|
||||
@@ -646,7 +647,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb->select('u')
|
||||
->from(CmsUser::class, 'u');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->setFirstResult(2);
|
||||
$criteria->setMaxResults(10);
|
||||
|
||||
@@ -664,7 +665,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
->setFirstResult(2)
|
||||
->setMaxResults(10);
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
|
||||
$qb->addCriteria($criteria);
|
||||
|
||||
@@ -954,7 +955,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb->select('alias1')->from(CmsUser::class, 'alias1');
|
||||
$qb->join('alias1.articles', 'alias2');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->eq('field', 'value1'));
|
||||
$criteria->andWhere($criteria->expr()->gt('alias2.field', 'value2'));
|
||||
|
||||
@@ -972,7 +973,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb->select('alias1')->from(CmsUser::class, 'alias1');
|
||||
$qb->join('alias1.articles', 'alias2');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->eq('alias1.field', 'value1'));
|
||||
$criteria->andWhere($criteria->expr()->gt('alias2.field', 'value2'));
|
||||
|
||||
@@ -990,7 +991,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$qb->select('alias1')->from(CmsUser::class, 'alias1');
|
||||
$qb->join('alias1.articles', 'alias2');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria = defined(Criteria::class . '::ASC') ? Criteria::create(true) : Criteria::create();
|
||||
$criteria->where($criteria->expr()->eq('alias1.field', 'value1'));
|
||||
$criteria->andWhere($criteria->expr()->gt('alias2.field', 'value2'));
|
||||
$criteria->andWhere($criteria->expr()->lt('alias2.field', 'value3'));
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Tools\CursorPagination;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\InverseJoinColumn;
|
||||
use Doctrine\ORM\Mapping\JoinColumn;
|
||||
use Doctrine\ORM\Mapping\JoinTable;
|
||||
use Doctrine\ORM\Mapping\ManyToMany;
|
||||
use Doctrine\ORM\Mapping\ManyToOne;
|
||||
use Doctrine\ORM\Mapping\MappedSuperclass;
|
||||
use Doctrine\ORM\Mapping\OneToOne;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
|
||||
abstract class CursorPaginationTestCase extends OrmTestCase
|
||||
{
|
||||
/** @var EntityManagerInterface */
|
||||
public $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->entityManager = $this->getTestEntityManager();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[Entity]
|
||||
class MyBlogPost
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
public $id;
|
||||
|
||||
/** @var Author */
|
||||
#[ManyToOne(targetEntity: 'Author')]
|
||||
public $author;
|
||||
|
||||
/** @var Category */
|
||||
#[ManyToOne(targetEntity: 'Category')]
|
||||
public $category;
|
||||
|
||||
/** @var string */
|
||||
#[Column(type: 'string', length: 255)]
|
||||
public $title;
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class MyAuthor
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
public $id;
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class MyCategory
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
public $id;
|
||||
}
|
||||
|
||||
|
||||
#[Entity]
|
||||
class BlogPost
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
public $id;
|
||||
|
||||
/** @var Author */
|
||||
#[ManyToOne(targetEntity: 'Author')]
|
||||
public $author;
|
||||
|
||||
/** @var Category */
|
||||
#[ManyToOne(targetEntity: 'Category')]
|
||||
public $category;
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class Author
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
public $id;
|
||||
|
||||
/** @var string */
|
||||
#[Column(type: 'string', length: 255)]
|
||||
public $name;
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class Person
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
public $id;
|
||||
|
||||
/** @var string */
|
||||
#[Column(type: 'string', length: 255)]
|
||||
public $name;
|
||||
|
||||
/** @var string */
|
||||
#[Column(type: 'string', length: 255)]
|
||||
public $biography;
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class Category
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
public $id;
|
||||
}
|
||||
|
||||
|
||||
#[Table(name: 'groups')]
|
||||
#[Entity]
|
||||
class Group
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
public $id;
|
||||
|
||||
/** @phpstan-var Collection<int, User> */
|
||||
#[ManyToMany(targetEntity: 'User', mappedBy: 'groups')]
|
||||
public $users;
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class User
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
public $id;
|
||||
|
||||
/** @phpstan-var Collection<int, Group> */
|
||||
#[JoinTable(name: 'user_group')]
|
||||
#[JoinColumn(name: 'user_id', referencedColumnName: 'id')]
|
||||
#[InverseJoinColumn(name: 'group_id', referencedColumnName: 'id')]
|
||||
#[ManyToMany(targetEntity: 'Group', inversedBy: 'users')]
|
||||
public $groups;
|
||||
|
||||
/** @var Avatar */
|
||||
#[OneToOne(targetEntity: 'Avatar', mappedBy: 'user')]
|
||||
public $avatar;
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class Avatar
|
||||
{
|
||||
/** @var int */
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
public $id;
|
||||
|
||||
/** @var User */
|
||||
#[OneToOne(targetEntity: 'User', inversedBy: 'avatar')]
|
||||
#[JoinColumn(name: 'user_id', referencedColumnName: 'id')]
|
||||
public $user;
|
||||
|
||||
/** @var string */
|
||||
#[Column(type: 'string', length: 255)]
|
||||
public $image;
|
||||
|
||||
/** @var int */
|
||||
#[Column(type: 'integer')]
|
||||
public $imageHeight;
|
||||
|
||||
/** @var int */
|
||||
#[Column(type: 'integer')]
|
||||
public $imageWidth;
|
||||
|
||||
/** @var string */
|
||||
#[Column(type: 'string', length: 255)]
|
||||
public $imageAltDesc;
|
||||
}
|
||||
|
||||
#[MappedSuperclass]
|
||||
abstract class Identified
|
||||
{
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
private int $id;
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class Banner extends Identified
|
||||
{
|
||||
/** @var string */
|
||||
#[Column(type: 'string', length: 255)]
|
||||
public $name;
|
||||
}
|
||||
402
tests/Tests/ORM/Tools/CursorPagination/CursorPaginatorTest.php
Normal file
402
tests/Tests/ORM/Tools/CursorPagination/CursorPaginatorTest.php
Normal file
@@ -0,0 +1,402 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Tools\CursorPagination;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Schema\Name\UnquotedIdentifierFolding;
|
||||
use Doctrine\ORM\Decorator\EntityManagerDecorator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Tools\CursorPagination\Cursor;
|
||||
use Doctrine\ORM\Tools\CursorPagination\CursorItem;
|
||||
use Doctrine\ORM\Tools\CursorPagination\CursorPaginator;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
use LogicException;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
use function enum_exists;
|
||||
|
||||
class CursorPaginatorTest extends OrmTestCase
|
||||
{
|
||||
private Connection&MockObject $connection;
|
||||
private EntityManagerInterface&MockObject $em;
|
||||
private AbstractHydrator&MockObject $hydrator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$platform = $this->getMockBuilder(AbstractPlatform::class)
|
||||
->setConstructorArgs(enum_exists(UnquotedIdentifierFolding::class) ? [UnquotedIdentifierFolding::UPPER] : [])
|
||||
->getMock();
|
||||
$platform->method('supportsIdentityColumns')
|
||||
->willReturn(true);
|
||||
|
||||
$driver = $this->createMock(Driver::class);
|
||||
$driver->method('getDatabasePlatform')
|
||||
->willReturn($platform);
|
||||
|
||||
$this->connection = $this->getMockBuilder(Connection::class)
|
||||
->onlyMethods(['executeQuery'])
|
||||
->setConstructorArgs([[], $driver])
|
||||
->getMock();
|
||||
|
||||
$this->em = $this->getMockBuilder(EntityManagerDecorator::class)
|
||||
->onlyMethods(['newHydrator'])
|
||||
->setConstructorArgs([$this->createTestEntityManagerWithConnection($this->connection)])
|
||||
->getMock();
|
||||
|
||||
$this->hydrator = $this->createMock(AbstractHydrator::class);
|
||||
$this->em->method('newHydrator')->willReturn($this->hydrator);
|
||||
}
|
||||
|
||||
public function testPaginatorAcceptsQueryBuilder(): void
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder()
|
||||
->select('p')
|
||||
->from(BlogPost::class, 'p')
|
||||
->orderBy('p.id', 'ASC');
|
||||
|
||||
$paginator = new CursorPaginator($qb);
|
||||
|
||||
self::assertInstanceOf(Query::class, $paginator->getQuery());
|
||||
}
|
||||
|
||||
public function testHasNextPageWhenMoreResultsExist(): void
|
||||
{
|
||||
$items = [
|
||||
(object) ['id' => 1],
|
||||
(object) ['id' => 2],
|
||||
(object) ['id' => 3],
|
||||
(object) ['id' => 4],
|
||||
];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 3);
|
||||
|
||||
self::assertTrue($paginator->hasNextPage());
|
||||
self::assertFalse($paginator->hasPreviousPage());
|
||||
self::assertCount(3, $paginator);
|
||||
}
|
||||
|
||||
public function testHasNoPagesOnFirstPageWithoutMoreResults(): void
|
||||
{
|
||||
$items = [(object) ['id' => 1]];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
self::assertFalse($paginator->hasPreviousPage());
|
||||
self::assertFalse($paginator->hasNextPage());
|
||||
self::assertFalse($paginator->hasToPaginate());
|
||||
}
|
||||
|
||||
public function testGetNextCursorAsStringThrowsWhenNoNextPage(): void
|
||||
{
|
||||
$items = [(object) ['id' => 1]];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$paginator->getNextCursorAsString();
|
||||
}
|
||||
|
||||
public function testGetPreviousCursorAsStringThrowsWhenNoPreviousPage(): void
|
||||
{
|
||||
$items = [(object) ['id' => 1]];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$paginator->getPreviousCursorAsString();
|
||||
}
|
||||
|
||||
public function testGetNextCursorThrowsWhenNoNextPage(): void
|
||||
{
|
||||
$items = [(object) ['id' => 1]];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$paginator->getNextCursor();
|
||||
}
|
||||
|
||||
public function testGetPreviousCursorThrowsWhenNoPreviousPage(): void
|
||||
{
|
||||
$items = [(object) ['id' => 1]];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$paginator->getPreviousCursor();
|
||||
}
|
||||
|
||||
public function testGetNextCursorWhenMoreResultsExist(): void
|
||||
{
|
||||
$items = [
|
||||
(object) ['id' => 1],
|
||||
(object) ['id' => 2],
|
||||
];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 1);
|
||||
|
||||
$nextCursor = $paginator->getNextCursor();
|
||||
|
||||
self::assertTrue($nextCursor->isNext());
|
||||
}
|
||||
|
||||
public function testGetNextCursorAsStringReturnsStringWhenNextPageExists(): void
|
||||
{
|
||||
$items = [
|
||||
(object) ['id' => 1],
|
||||
(object) ['id' => 2],
|
||||
];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 1);
|
||||
|
||||
self::assertIsString($paginator->getNextCursorAsString());
|
||||
}
|
||||
|
||||
public function testGetPreviousCursorAsStringReturnsStringWhenPreviousPageExists(): void
|
||||
{
|
||||
$items = [
|
||||
(object) ['id' => 1],
|
||||
(object) ['id' => 2],
|
||||
];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$cursor = (new Cursor(['p__id' => 1], true))->encodeToString();
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate($cursor, 10);
|
||||
|
||||
self::assertIsString($paginator->getPreviousCursorAsString());
|
||||
}
|
||||
|
||||
public function testEmptyResultSet(): void
|
||||
{
|
||||
$this->hydrator->method('hydrateAll')->willReturn([]);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
self::assertCount(0, $paginator);
|
||||
self::assertFalse($paginator->hasNextPage());
|
||||
self::assertFalse($paginator->hasPreviousPage());
|
||||
}
|
||||
|
||||
public function testIteratorReturnsItems(): void
|
||||
{
|
||||
$items = [
|
||||
(object) ['id' => 1],
|
||||
(object) ['id' => 2],
|
||||
];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
$iteratedItems = [];
|
||||
foreach ($paginator as $item) {
|
||||
$iteratedItems[] = $item;
|
||||
}
|
||||
|
||||
self::assertCount(2, $iteratedItems);
|
||||
}
|
||||
|
||||
public function testGetValuesReturnsRawEntities(): void
|
||||
{
|
||||
$items = [
|
||||
(object) ['id' => 1],
|
||||
(object) ['id' => 2],
|
||||
];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
$values = $paginator->getValues();
|
||||
|
||||
self::assertCount(2, $values);
|
||||
self::assertSame($items[0], $values[0]);
|
||||
self::assertSame($items[1], $values[1]);
|
||||
}
|
||||
|
||||
public function testGetItemsReturnsCursorItems(): void
|
||||
{
|
||||
$items = [
|
||||
(object) ['id' => 1],
|
||||
(object) ['id' => 2],
|
||||
];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
$cursorItems = $paginator->getItems();
|
||||
|
||||
self::assertCount(2, $cursorItems);
|
||||
self::assertInstanceOf(CursorItem::class, $cursorItems[0]);
|
||||
self::assertSame($items[0], $cursorItems[0]->getValue());
|
||||
self::assertInstanceOf(Cursor::class, $cursorItems[0]->getCursor());
|
||||
self::assertSame($items[1], $cursorItems[1]->getValue());
|
||||
}
|
||||
|
||||
public function testGetItemsReturnsEmptyArrayWhenNoResults(): void
|
||||
{
|
||||
$this->hydrator->method('hydrateAll')->willReturn([]);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
self::assertSame([], $paginator->getItems());
|
||||
self::assertSame([], $paginator->getValues());
|
||||
}
|
||||
|
||||
public function testPaginateWithEmptyCursorTreatedAsFirstPage(): void
|
||||
{
|
||||
$items = [(object) ['id' => 1]];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate('', 10);
|
||||
|
||||
self::assertFalse($paginator->hasPreviousPage());
|
||||
self::assertFalse($paginator->hasNextPage());
|
||||
}
|
||||
|
||||
public function testPaginateWithPreviousCursorReversesItems(): void
|
||||
{
|
||||
$items = [
|
||||
(object) ['id' => 3],
|
||||
(object) ['id' => 2],
|
||||
(object) ['id' => 1],
|
||||
];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
|
||||
$previousCursor = (new Cursor(['p__id' => 3], false))->encodeToString();
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate($previousCursor, 10);
|
||||
|
||||
$values = $paginator->getValues();
|
||||
self::assertCount(3, $values);
|
||||
self::assertSame(1, $values[0]->id);
|
||||
self::assertSame(2, $values[1]->id);
|
||||
self::assertSame(3, $values[2]->id);
|
||||
}
|
||||
|
||||
public function testCloneQueryCopiesExistingHints(): void
|
||||
{
|
||||
$items = [(object) ['id' => 1]];
|
||||
$this->hydrator->method('hydrateAll')->willReturn($items);
|
||||
$result = $this->createMock(Result::class);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
|
||||
$query = new Query($this->em);
|
||||
$query->setDQL('SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC');
|
||||
$query->setHint('custom.hint', 'custom_value');
|
||||
|
||||
$paginator = new CursorPaginator($query);
|
||||
$paginator->paginate(null, 10);
|
||||
|
||||
self::assertCount(1, $paginator);
|
||||
}
|
||||
}
|
||||
97
tests/Tests/ORM/Tools/CursorPagination/CursorTest.php
Normal file
97
tests/Tests/ORM/Tools/CursorPagination/CursorTest.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Tools\CursorPagination;
|
||||
|
||||
use Doctrine\ORM\Tools\CursorPagination\Cursor;
|
||||
use Doctrine\ORM\Tools\CursorPagination\Exception\InvalidCursor;
|
||||
use JsonException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
|
||||
use function base64_encode;
|
||||
use function json_encode;
|
||||
use function rtrim;
|
||||
use function strtr;
|
||||
|
||||
class CursorTest extends TestCase
|
||||
{
|
||||
public function testCursorIsFinal(): void
|
||||
{
|
||||
$reflection = new ReflectionClass(Cursor::class);
|
||||
|
||||
self::assertTrue($reflection->isFinal());
|
||||
}
|
||||
|
||||
public function testEncodeAndDecode(): void
|
||||
{
|
||||
$parameters = ['id' => 10, 'name' => 'test'];
|
||||
$cursor = new Cursor($parameters, true);
|
||||
|
||||
$decoded = Cursor::fromEncodedString($cursor->encodeToString());
|
||||
|
||||
self::assertSame($parameters, $decoded->getParameters());
|
||||
self::assertTrue($decoded->isNext());
|
||||
}
|
||||
|
||||
public function testEncodeAndDecodeWithPreviousCursor(): void
|
||||
{
|
||||
$cursor = new Cursor(['id' => 10], false);
|
||||
|
||||
$decoded = Cursor::fromEncodedString($cursor->encodeToString());
|
||||
|
||||
self::assertTrue($decoded->isPrevious());
|
||||
}
|
||||
|
||||
public function testEncodeProducesUrlSafeString(): void
|
||||
{
|
||||
$cursor = new Cursor(['id' => 123456789, 'foo' => 'bar/baz+test']);
|
||||
$encoded = $cursor->encodeToString();
|
||||
|
||||
self::assertStringNotContainsString('+', $encoded);
|
||||
self::assertStringNotContainsString('/', $encoded);
|
||||
self::assertStringNotContainsString('=', $encoded);
|
||||
}
|
||||
|
||||
public function testFromEncodedStringThrowsForInvalidBase64(): void
|
||||
{
|
||||
$this->expectException(InvalidCursor::class);
|
||||
|
||||
Cursor::fromEncodedString('not-valid-base64!@#$');
|
||||
}
|
||||
|
||||
public function testFromEncodedStringThrowsForInvalidJson(): void
|
||||
{
|
||||
$this->expectException(InvalidCursor::class);
|
||||
|
||||
Cursor::fromEncodedString(rtrim(strtr(base64_encode('not-json'), '+/', '-_'), '='));
|
||||
}
|
||||
|
||||
public function testFromEncodedStringChainsPreviousExceptionForInvalidJson(): void
|
||||
{
|
||||
try {
|
||||
Cursor::fromEncodedString(rtrim(strtr(base64_encode('not-json'), '+/', '-_'), '='));
|
||||
self::fail('Expected InvalidCursor to be thrown.');
|
||||
} catch (InvalidCursor $e) {
|
||||
self::assertInstanceOf(JsonException::class, $e->getPrevious());
|
||||
}
|
||||
}
|
||||
|
||||
public function testFromEncodedStringDefaultsToNextWhenIsNextMissing(): void
|
||||
{
|
||||
$json = json_encode(['id' => 10]);
|
||||
$encoded = rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
|
||||
|
||||
$cursor = Cursor::fromEncodedString($encoded);
|
||||
|
||||
self::assertTrue($cursor->isNext());
|
||||
}
|
||||
|
||||
public function testToArray(): void
|
||||
{
|
||||
$cursor = new Cursor(['id' => 1], true);
|
||||
|
||||
self::assertSame(['id' => 1, '_isNext' => true], $cursor->toArray());
|
||||
}
|
||||
}
|
||||
179
tests/Tests/ORM/Tools/CursorPagination/CursorWalkerTest.php
Normal file
179
tests/Tests/ORM/Tools/CursorPagination/CursorWalkerTest.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Tools\CursorPagination;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Tools\CursorPagination\CursorWalker;
|
||||
use LogicException;
|
||||
|
||||
class CursorWalkerTest extends CursorPaginationTestCase
|
||||
{
|
||||
public function testThrowsExceptionWithoutOrderBy(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage('No ORDER BY clause found. Cursor pagination requires a deterministic sort order.');
|
||||
|
||||
$query->getSQL();
|
||||
}
|
||||
|
||||
public function testBasicQueryWithOrderBy(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, false);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, []);
|
||||
|
||||
self::assertEquals(
|
||||
'SELECT b0_.id AS id_0, b0_.author_id AS author_id_1, b0_.category_id AS category_id_2 FROM BlogPost b0_ ORDER BY b0_.id ASC',
|
||||
$query->getSQL(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testQueryWithReversedOrderBy(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, true);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, []);
|
||||
|
||||
self::assertEquals(
|
||||
'SELECT b0_.id AS id_0, b0_.author_id AS author_id_1, b0_.category_id AS category_id_2 FROM BlogPost b0_ ORDER BY b0_.id DESC',
|
||||
$query->getSQL(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testQueryWithMultipleOrderByColumnsReversed(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\MyBlogPost p ORDER BY p.title ASC, p.id DESC',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, true);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, []);
|
||||
|
||||
self::assertEquals(
|
||||
'SELECT m0_.id AS id_0, m0_.title AS title_1, m0_.author_id AS author_id_2, m0_.category_id AS category_id_3 FROM MyBlogPost m0_ ORDER BY m0_.title DESC, m0_.id ASC',
|
||||
$query->getSQL(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testCursorConditionSingleColumnAsc(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id ASC',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, false);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, ['p.id' => 10]);
|
||||
|
||||
self::assertEquals(
|
||||
'SELECT b0_.id AS id_0, b0_.author_id AS author_id_1, b0_.category_id AS category_id_2 FROM BlogPost b0_ WHERE (b0_.id > ?) ORDER BY b0_.id ASC',
|
||||
$query->getSQL(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testCursorConditionSingleColumnDesc(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p ORDER BY p.id DESC',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, false);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, ['p.id' => 10]);
|
||||
|
||||
self::assertEquals(
|
||||
'SELECT b0_.id AS id_0, b0_.author_id AS author_id_1, b0_.category_id AS category_id_2 FROM BlogPost b0_ WHERE (b0_.id < ?) ORDER BY b0_.id DESC',
|
||||
$query->getSQL(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testCursorConditionWithExistingWhere(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p WHERE p.id > 5 ORDER BY p.id ASC',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, false);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, ['p.id' => 10]);
|
||||
|
||||
self::assertEquals(
|
||||
'SELECT b0_.id AS id_0, b0_.author_id AS author_id_1, b0_.category_id AS category_id_2 FROM BlogPost b0_ WHERE b0_.id > 5 AND (b0_.id > ?) ORDER BY b0_.id ASC',
|
||||
$query->getSQL(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testQueryWithJoinAndOrderByJoinedEntity(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p, a FROM Doctrine\Tests\ORM\Tools\CursorPagination\BlogPost p JOIN p.author a ORDER BY a.name ASC, p.id ASC',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, false);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, []);
|
||||
|
||||
self::assertEquals(
|
||||
'SELECT b0_.id AS id_0, a1_.id AS id_1, a1_.name AS name_2, b0_.author_id AS author_id_3, b0_.category_id AS category_id_4 FROM BlogPost b0_ INNER JOIN Author a1_ ON b0_.author_id = a1_.id ORDER BY a1_.name ASC, b0_.id ASC',
|
||||
$query->getSQL(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testCursorConditionMultipleColumnsDesc(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\MyBlogPost p ORDER BY p.title DESC, p.id DESC',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, false);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, ['p.title' => 'Test', 'p.id' => 10]);
|
||||
|
||||
self::assertEquals(
|
||||
'SELECT m0_.id AS id_0, m0_.title AS title_1, m0_.author_id AS author_id_2, m0_.category_id AS category_id_3 FROM MyBlogPost m0_ WHERE (m0_.title < ? OR (m0_.title = ? AND (m0_.id < ?))) ORDER BY m0_.title DESC, m0_.id DESC',
|
||||
$query->getSQL(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testCursorConditionMultipleColumnsAsc(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT a FROM Doctrine\Tests\ORM\Tools\CursorPagination\Author a ORDER BY a.name ASC, a.id ASC',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, false);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, ['a.name' => 'John', 'a.id' => 5]);
|
||||
|
||||
self::assertEquals(
|
||||
'SELECT a0_.id AS id_0, a0_.name AS name_1 FROM Author a0_ WHERE (a0_.name > ? OR (a0_.name = ? AND (a0_.id > ?))) ORDER BY a0_.name ASC, a0_.id ASC',
|
||||
$query->getSQL(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testCursorConditionThreeColumnsDesc(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\CursorPagination\Person p ORDER BY p.biography DESC, p.name DESC, p.id DESC',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CursorWalker::class]);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_REVERSE, false);
|
||||
$query->setHint(CursorWalker::HINT_CURSOR_PARAMETERS, [
|
||||
'p.biography' => '2019-11-04',
|
||||
'p.name' => 'test@example.com',
|
||||
'p.id' => 529,
|
||||
]);
|
||||
|
||||
self::assertEquals(
|
||||
'SELECT p0_.id AS id_0, p0_.name AS name_1, p0_.biography AS biography_2 FROM Person p0_ WHERE (p0_.biography < ? OR (p0_.biography = ? AND (p0_.name < ? OR (p0_.name = ? AND (p0_.id < ?))))) ORDER BY p0_.biography DESC, p0_.name DESC, p0_.id DESC',
|
||||
$query->getSQL(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user