Skip to content

Commit 2a30dd7

Browse files
committed
Add Doctrine DBAL v4 / ORM v3 support, remove PHP 8.0 support
1 parent 4e0c27e commit 2a30dd7

26 files changed

+502
-130
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ jobs:
1414
matrix:
1515
include:
1616
#Mini (for each Symfony version)
17-
- php-version: '8.0'
18-
symfony-version: '5.4.*'
19-
composer-flags: '--prefer-stable --prefer-lowest'
20-
description: 'with SF 5.4.* lowest'
2117
- php-version: '8.1'
2218
symfony-version: '6.4.*'
2319
composer-flags: '--prefer-stable --prefer-lowest'
@@ -61,7 +57,6 @@ jobs:
6157
description: 'with SF 7.2.* dev'
6258

6359
#PHP versions
64-
- php-version: '8.0'
6560
- php-version: '8.1'
6661
- php-version: '8.2'
6762
- php-version: '8.3'
@@ -72,7 +67,7 @@ jobs:
7267
coding-standards: true
7368

7469
#Static Analysis (min PHP version)
75-
- php-version: '8.0'
70+
- php-version: '8.1'
7671
description: 'with Static Analysis'
7772
static-analysis: true
7873

.php-cs-fixer.dist.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
$finder = PhpCsFixer\Finder::create()
1313
->in(__DIR__)
14+
->exclude('tests/App/Logging') // Compatibility DBAL 3/4
1415
;
1516
$config = new PhpCsFixer\Config();
1617

@@ -19,8 +20,8 @@
1920
'@Symfony' => true,
2021
'@Symfony:risky' => true,
2122
'@DoctrineAnnotation' => true,
22-
'@PHP80Migration' => true,
23-
'@PHP74Migration:risky' => true,
23+
'@PHP81Migration' => true,
24+
'@PHP80Migration:risky' => true,
2425
'@PHPUnit84Migration:risky' => true,
2526
'array_syntax' => ['syntax' => 'short'],
2627
'fopen_flags' => true,

UPGRADE-2.0.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# UPGRADE FROM 1.x to 2.0
2+
3+
# QueryBuilder - Count with DBAL QueryBuilder
4+
5+
Some changes when using the "count" feature with a **DBAL** query builder :
6+
7+
* The default behavior is no longer `count_by_sub_request` but is now `count_by_select_all` (new behavior that performs a `SELECT count(*) FROM ...`).
8+
* When the `count_by_sub_request` behavior is used, the `connection` option (an instance of `Doctrine\DBAL\Connection`) is now required.
9+
10+
Refer to the [full documentation](doc/count.md) for available options.

composer.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@
1818
},
1919
"require": {
2020
"php": "^8.0",
21-
"doctrine/dbal": "^2.11.0|^3.0",
21+
"doctrine/dbal": "^3.8|^4.0",
2222
"ecommit/paginator": "^1.0",
2323
"symfony/options-resolver": "^5.4|^6.4|^7.0"
2424
},
2525
"require-dev": {
26-
"doctrine/annotations": "*",
2726
"doctrine/orm": "*",
2827
"friendsofphp/php-cs-fixer": "^3.0",
2928
"phpstan/phpstan": "^1.9",
@@ -34,14 +33,14 @@
3433
"doctrine/orm": "Doctrine ORM"
3534
},
3635
"conflict": {
37-
"doctrine/orm": "<2.12.0 || >=3.0.0"
36+
"doctrine/orm": "<2.19.0 || >=4.0.0"
3837
},
3938
"config": {
4039
"sort-packages": true
4140
},
4241
"extra": {
4342
"branch-alias": {
44-
"dev-master": "1.x-dev"
43+
"dev-master": "2.x-dev"
4544
}
4645
}
4746
}

doc/count.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ $count = DoctrinePaginatorBuilder::countQueryBuilder([
1515

1616
**Available options :**
1717

18-
| Option | Type | Required | Default value | Description |
19-
| --- | --- | --- | --- | --- |
20-
| **query_builder** | `Doctrine\DBAL\Query\QueryBuilder` | Yes | | QueryBuilder |
21-
| **behavior** | String | No | `count_by_sub_request` | Method used to count results. Available values: <ul><li>`count_by_alias`: Use a alias (`SELECT count(alias) FROM ...`) *(`alias` option is required)*</li><li>`count_by_sub_request` : Use a sub request</li></ul> |
22-
| **alias** | String | Only if `behavior = count_by_alias` | | Can only be used when `behavior = count_by_alias` |
23-
| **distinct_alias** | Bool | No | `true` | Use `DISTINCT` (`SELECT count(DISTINCT alias) FROM ...`). Can only be used when `behavior = count_by_alias` |
24-
18+
| Option | Type | Required | Default value | Description |
19+
|--------------------| --- | --- | --- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
20+
| **query_builder** | `Doctrine\DBAL\Query\QueryBuilder` | Yes | | QueryBuilder |
21+
| **behavior** | String | No | `count_by_select_all` | Method used to count results. Available values: <ul><li>`count_by_alias`: Use a alias (`SELECT count(alias) FROM ...`) *(`alias` option is required)*</li><li>`count_by_sub_request` : Use a sub request *(`connection` option is required)*</li><li>`count_by_select_all`: Use `SELECT count(*) FROM ...`)</li></ul> |
22+
| **alias** | String | Only if `behavior = count_by_alias` | | Can only be used when `behavior = count_by_alias` |
23+
| **distinct_alias** | Bool | No | `true` | Use `DISTINCT` (`SELECT count(DISTINCT alias) FROM ...`). Can only be used when `behavior = count_by_alias` |
24+
| **connection** | `Doctrine\DBAL\Connection` | Only if `behavior = count_by_sub_request` | | Can only be used when `behavior = count_by_sub_request` |
2525

2626

2727
## With ORM QueryBuilder

phpstan-baseline.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ parameters:
66
-
77
message: '#expects TOptions of array<string, mixed>, array<string, mixed> given#'
88
path: tests/
9+
10+
excludePaths:
11+
# Compatibility DBAL 3/4
12+
- tests/App/Logging

src/Paginator/DoctrineDBALPaginator.php

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
namespace Ecommit\DoctrineUtils\Paginator;
1515

1616
use Doctrine\DBAL\Query\QueryBuilder;
17-
use Doctrine\DBAL\Result;
1817
use Ecommit\DoctrineUtils\QueryBuilderFilter;
1918
use Symfony\Component\OptionsResolver\OptionsResolver;
2019

@@ -65,32 +64,23 @@ protected function buildIterator(): \Traversable
6564
$byIdentifier = $this->getOption('by_identifier');
6665
if (null === $byIdentifier) {
6766
$this->setOffsetAndLimit($queryBuilder);
68-
$result = $queryBuilder->execute();
69-
if (!$result instanceof Result) {
70-
throw new \Exception('Expected class : '.Result::class);
71-
}
67+
$result = $queryBuilder->executeQuery();
7268

7369
return new \ArrayIterator($result->fetchAllAssociative());
7470
}
7571

7672
$idsQueryBuilder = clone $queryBuilder;
7773
$idsQueryBuilder->select(\sprintf('DISTINCT %s as pk', $this->getOption('by_identifier')));
7874
$this->setOffsetAndLimit($idsQueryBuilder);
79-
$result = $idsQueryBuilder->execute();
80-
if (!$result instanceof Result) {
81-
throw new \Exception('Expected class : '.Result::class);
82-
}
75+
$result = $idsQueryBuilder->executeQuery();
8376

8477
$ids = $result->fetchFirstColumn();
8578

8679
$resultsByIdsQueryBuilder = clone $queryBuilder;
87-
$resultsByIdsQueryBuilder->resetQueryPart('where');
80+
$resultsByIdsQueryBuilder->resetWhere();
8881
$resultsByIdsQueryBuilder->setParameters([]);
8982
QueryBuilderFilter::addMultiFilter($resultsByIdsQueryBuilder, QueryBuilderFilter::SELECT_IN, $ids, $byIdentifier, 'paginate_pks');
90-
$result = $resultsByIdsQueryBuilder->execute();
91-
if (!$result instanceof Result) {
92-
throw new \Exception('Expected class : '.Result::class);
93-
}
83+
$result = $resultsByIdsQueryBuilder->executeQuery();
9484

9585
return new \ArrayIterator($result->fetchAllAssociative());
9686
}

src/Paginator/DoctrineORMPaginator.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace Ecommit\DoctrineUtils\Paginator;
1515

16+
use Doctrine\Common\Collections\ArrayCollection;
1617
use Doctrine\ORM\QueryBuilder;
1718
use Doctrine\ORM\Tools\Pagination\Paginator;
1819
use Ecommit\DoctrineUtils\QueryBuilderFilter;
@@ -91,7 +92,7 @@ protected function buildIterator(): \Traversable
9192

9293
$resultsByIdsQueryBuilder = clone $queryBuilder;
9394
$resultsByIdsQueryBuilder->resetDQLPart('where');
94-
$resultsByIdsQueryBuilder->setParameters([]);
95+
$resultsByIdsQueryBuilder->setParameters(new ArrayCollection());
9596
QueryBuilderFilter::addMultiFilter($resultsByIdsQueryBuilder, QueryBuilderFilter::SELECT_IN, $ids, $byIdentifier, 'paginate_pks');
9697
$result = $resultsByIdsQueryBuilder->getQuery()->getResult();
9798
if (!\is_array($result)) {

src/Paginator/DoctrinePaginatorBuilder.php

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
namespace Ecommit\DoctrineUtils\Paginator;
1515

16+
use Doctrine\DBAL\Connection;
1617
use Doctrine\DBAL\Query\QueryBuilder as QueryBuilderDBAL;
17-
use Doctrine\DBAL\Result;
1818
use Doctrine\ORM\Query\ResultSetMapping;
1919
use Doctrine\ORM\QueryBuilder as QueryBuilderORM;
2020
use Doctrine\ORM\Tools\Pagination\Paginator;
@@ -26,17 +26,19 @@
2626
/**
2727
* @phpstan-type CountOptions array{
2828
* query_builder: QueryBuilderDBAL|QueryBuilderORM,
29-
* behavior?: 'count_by_alias'|'count_by_sub_request'|'orm',
29+
* behavior?: 'count_by_alias'|'count_by_sub_request'|'orm'|'count_by_select_all',
3030
* alias?: ?string,
3131
* distinct_alias?: ?bool,
32-
* simplified_request?: ?bool
32+
* simplified_request?: ?bool,
33+
* connection?: ?Connection
3334
* }
3435
* @phpstan-type CountResolvedOptions array{
3536
* query_builder: QueryBuilderDBAL|QueryBuilderORM,
36-
* behavior: 'count_by_alias'|'count_by_sub_request'|'orm',
37+
* behavior: 'count_by_alias'|'count_by_sub_request'|'orm'|'count_by_select_all',
3738
* alias: ?string,
3839
* distinct_alias: ?bool,
39-
* simplified_request: ?bool
40+
* simplified_request: ?bool,
41+
* connection: ?Connection
4042
* }
4143
*/
4244
class DoctrinePaginatorBuilder
@@ -48,11 +50,13 @@ class DoctrinePaginatorBuilder
4850
* * alias [ONLY WITH behavior=count_by_alias]
4951
* * distinct_alias [ONLY WITH behavior=count_by_alias]
5052
* * simplified_request - Remove unnecessary "select" statements [ONLY WITH ORM QUERY BUILDER AND WITH behavior=orm ]
53+
* * connection [ONLY WITH DBAL QUERY BUILDER]
5154
*
5255
* Availabled behaviors :
5356
* * count_by_alias: Use alias. Option "alias" is required
5457
* * count_by_sub_request: Use sub request
5558
* * orm: Use Doctrine ORM Paginator [ONLY WITH ORM QUERY BUILDER]
59+
* * count_by_select_all [ONLY WITH DBAL QUERY BUILDER]
5660
*
5761
* @return int<0, max>
5862
*/
@@ -70,12 +74,13 @@ final public static function countQueryBuilder(array $options): int
7074
'alias' => null,
7175
'distinct_alias' => null,
7276
'simplified_request' => null,
77+
'connection' => null,
7378
]);
7479
$resolver->setAllowedTypes('query_builder', [QueryBuilderDBAL::class, QueryBuilderORM::class]);
7580
$resolver->setAllowedTypes('behavior', 'string');
7681
$resolver->setAllowedValues('behavior', function (string $behavior) use ($options): bool {
7782
if ($options['query_builder'] instanceof QueryBuilderDBAL) {
78-
return \in_array($behavior, ['count_by_alias', 'count_by_sub_request']);
83+
return \in_array($behavior, ['count_by_alias', 'count_by_sub_request', 'count_by_select_all']);
7984
}
8085

8186
return \in_array($behavior, ['count_by_alias', 'count_by_sub_request', 'orm']);
@@ -113,6 +118,16 @@ final public static function countQueryBuilder(array $options): int
113118

114119
return $simplifiedRequest;
115120
});
121+
$resolver->setAllowedTypes('connection', [Connection::class, 'null']);
122+
$resolver->setNormalizer('connection', function (Options $options, ?Connection $connection): ?Connection {
123+
if ('count_by_sub_request' === $options['behavior'] && $options['query_builder'] instanceof QueryBuilderDBAL && null === $connection) {
124+
throw new MissingOptionsException('When "behavior" option is set to "count_by_sub_request" with DBAL QueryBuilder, "connection" option is required');
125+
} elseif (null !== $connection && !($options['query_builder'] instanceof QueryBuilderDBAL)) {
126+
throw new InvalidOptionsException('The "connection" option can only be used with DBAL QueryBuilder');
127+
}
128+
129+
return $connection;
130+
});
116131
/** @var CountResolvedOptions $options */
117132
$options = $resolver->resolve($options);
118133

@@ -155,11 +170,22 @@ private static function countQueryBuilderDBAL(QueryBuilderDBAL $queryBuilder, ar
155170

156171
$distinct = ($options['distinct_alias']) ? 'DISTINCT ' : '';
157172
$countQueryBuilder->select(\sprintf('count(%s%s)', $distinct, $options['alias']));
158-
$countQueryBuilder->resetQueryPart('orderBy');
159-
$result = $countQueryBuilder->execute();
160-
if (!$result instanceof Result) {
161-
throw new \Exception('Expected class : '.Result::class);
173+
$countQueryBuilder->resetOrderBy();
174+
$result = $countQueryBuilder->executeQuery();
175+
$count = $result->fetchOne();
176+
if (false === $count) {
177+
throw new \Exception('Mixed result expected');
162178
}
179+
/** @var int<0, max> $count */
180+
$count = (int) $count; // @phpstan-ignore-line
181+
182+
return $count;
183+
} elseif ('count_by_select_all' === $options['behavior']) {
184+
$countQueryBuilder = clone $queryBuilder;
185+
186+
$countQueryBuilder->select('count(*)');
187+
$countQueryBuilder->resetOrderBy();
188+
$result = $countQueryBuilder->executeQuery();
163189
$count = $result->fetchOne();
164190
if (false === $count) {
165191
throw new \Exception('Mixed result expected');
@@ -171,19 +197,17 @@ private static function countQueryBuilderDBAL(QueryBuilderDBAL $queryBuilder, ar
171197
}
172198

173199
// count_by_sub_request
174-
$queryBuilderCount = clone $queryBuilder;
175200
$queryBuilderClone = clone $queryBuilder;
201+
$queryBuilderClone->resetOrderBy();
176202

177-
$queryBuilderClone->resetQueryPart('orderBy');
178-
179-
$queryBuilderCount->resetQueryParts(); // Remove Query Parts
203+
/** @var Connection $connection */
204+
$connection = $options['connection'];
205+
$queryBuilderCount = $connection->createQueryBuilder();
180206
$queryBuilderCount->select('count(*)')
181-
->from('('.$queryBuilderClone->getSql().')', 'mainquery');
182-
$result = $queryBuilderCount->execute();
207+
->from('('.$queryBuilderClone->getSql().')', 'mainquery')
208+
->setParameters($queryBuilderClone->getParameters(), $queryBuilderClone->getParameterTypes());
209+
$result = $queryBuilderCount->executeQuery();
183210

184-
if (!$result instanceof Result) {
185-
throw new \Exception('Expected class : '.Result::class);
186-
}
187211
$count = $result->fetchOne();
188212
if (false === $count) {
189213
throw new \Exception('Mixed result expected');
@@ -248,7 +272,7 @@ private static function countQueryBuilderORM(QueryBuilderORM $queryBuilder, arra
248272
public static function getDefaultCountBehavior(QueryBuilderDBAL|QueryBuilderORM $queryBuilder): string
249273
{
250274
if ($queryBuilder instanceof QueryBuilderDBAL) {
251-
return 'count_by_sub_request';
275+
return 'count_by_select_all';
252276
}
253277

254278
return 'orm';

src/QueryBuilderFilter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace Ecommit\DoctrineUtils;
1515

16-
use Doctrine\DBAL\Connection;
16+
use Doctrine\DBAL\ArrayParameterType;
1717
use Doctrine\DBAL\Query\QueryBuilder as QueryBuilderDBAL;
1818
use Doctrine\ORM\QueryBuilder as QueryBuilderORM;
1919

@@ -80,7 +80,7 @@ private static function addSimpleMultiFilter(QueryBuilderDBAL|QueryBuilderORM $q
8080
$clauseSql = (self::SELECT_IN === $filterSign || self::SELECT_AUTO === $filterSign) ? 'IN' : 'NOT IN';
8181

8282
$queryBuilder->andWhere(\sprintf('%s %s (:%s)', $sqlField, $clauseSql, $paramName));
83-
$queryBuilder->setParameter($paramName, $filterValues, Connection::PARAM_STR_ARRAY);
83+
$queryBuilder->setParameter($paramName, $filterValues, ArrayParameterType::STRING);
8484

8585
return $queryBuilder;
8686
}
@@ -103,7 +103,7 @@ private static function addGroupMultiFilter(QueryBuilderDBAL|QueryBuilderORM $qu
103103
foreach (array_chunk($filterValues, self::MAX_PER_IN) as $filterValuesGroup) {
104104
++$groupNumber;
105105
$groups[] = \sprintf('%s %s (:%s%s)', $sqlField, $clauseSql, $paramName, $groupNumber);
106-
$queryBuilder->setParameter($paramName.$groupNumber, $filterValuesGroup, Connection::PARAM_STR_ARRAY);
106+
$queryBuilder->setParameter($paramName.$groupNumber, $filterValuesGroup, ArrayParameterType::STRING);
107107
}
108108

109109
$queryBuilder->andWhere(implode(' '.$separatorClauseSql.' ', $groups));

0 commit comments

Comments
 (0)