diff --git a/UPGRADE.md b/UPGRADE.md index 084d4a68cb5..c27231a450c 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -8,6 +8,23 @@ awareness about deprecated code. # Upgrade to 3.8 +## Deprecated reset methods from `QueryBuilder` + +`QueryBuilder::resetQueryParts()` has been deprecated. + +Resetting individual query parts through the generic `resetQueryPart()` method has been deprecated as well. +However, several replacements have been put in place depending on the `$queryPartName` parameter: + +| `$queryPartName` | suggested replacement | +|------------------|--------------------------------------------| +| `'select'` | Call `select()` with a new set of columns. | +| `'distinct'` | `distinct(false)` | +| `'where'` | `resetWhere()` | +| `'groupBy'` | `resetGroupBy()` | +| `'having'` | `resetHaving()` | +| `'orderBy'` | `resetOrderBy()` | +| `'values'` | Call `values()` with a new set of values. | + ## Deprecated getting query parts from `QueryBuilder` The usage of `QueryBuilder::getQueryPart()` and `::getQueryParts()` is deprecated. The query parts diff --git a/phpstan.neon.dist b/phpstan.neon.dist index ebda6402cbb..3074c054646 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -112,6 +112,12 @@ parameters: paths: - src/Platforms/AbstractPlatform.php + # Deprecated method, will be removed in 4.0.0 + - + message: '~^Variable method call on \$this\(Doctrine\\DBAL\\Query\\QueryBuilder\)\.$~' + paths: + - src/Query/QueryBuilder.php + # There is no way to make this assertion in the code, # and the API doesn't support parametrization of returned column types. - diff --git a/psalm.xml.dist b/psalm.xml.dist index 88948561586..bfeea5a7222 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -504,6 +504,8 @@ --> + + @@ -658,6 +660,12 @@ + + + + + + diff --git a/src/Query/QueryBuilder.php b/src/Query/QueryBuilder.php index c0d18d5f2ff..28ad02585a0 100644 --- a/src/Query/QueryBuilder.php +++ b/src/Query/QueryBuilder.php @@ -17,14 +17,17 @@ use function array_keys; use function array_unshift; use function count; +use function func_get_arg; use function func_get_args; use function func_num_args; use function implode; use function is_array; use function is_object; use function key; +use function method_exists; use function strtoupper; use function substr; +use function ucfirst; /** * QueryBuilder class is responsible to dynamically create SQL queries. @@ -35,6 +38,8 @@ * The query builder does no validation whatsoever if certain features even work with the * underlying database vendor. Limit queries and joins are NOT applied to UPDATE and DELETE statements * even if some vendors such as MySQL support it. + * + * @method $this distinct(bool $distinct = true) Adds or removes DISTINCT to/from the query. */ class QueryBuilder { @@ -677,7 +682,7 @@ public function select($select = null/*, string ...$selects*/) } /** - * Adds DISTINCT to the query. + * Adds or removes DISTINCT to/from the query. * * * $qb = $conn->createQueryBuilder() @@ -688,9 +693,10 @@ public function select($select = null/*, string ...$selects*/) * * @return $this This QueryBuilder instance. */ - public function distinct(): self + public function distinct(/* bool $distinct = true */): self { - $this->sqlParts['distinct'] = true; + $this->sqlParts['distinct'] = func_num_args() < 1 || func_get_arg(0); + $this->state = self::STATE_DIRTY; return $this; } @@ -1335,30 +1341,77 @@ public function getQueryParts() /** * Resets SQL parts. * + * @deprecated Use the dedicated reset*() methods instead. + * * @param string[]|null $queryPartNames * * @return $this This QueryBuilder instance. */ public function resetQueryParts($queryPartNames = null) { + Deprecation::trigger( + 'doctrine/dbal', + 'TODO', + '%s() is deprecated, instead use dedicated reset methods for the parts that shall be reset.', + __METHOD__, + ); + $queryPartNames ??= array_keys($this->sqlParts); foreach ($queryPartNames as $queryPartName) { - $this->resetQueryPart($queryPartName); + $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName]; } + $this->state = self::STATE_DIRTY; + return $this; } /** * Resets a single SQL part. * + * @deprecated Use the dedicated reset*() methods instead. + * * @param string $queryPartName * * @return $this This QueryBuilder instance. */ public function resetQueryPart($queryPartName) { + if ($queryPartName === 'distinct') { + Deprecation::trigger( + 'doctrine/dbal', + 'TODO', + 'Calling %s() with "distinct" is deprecated, call distinct(false) instead.', + __METHOD__, + ); + + return $this->distinct(false); + } + + $newMethodName = 'reset' . ucfirst($queryPartName); + if (array_key_exists($queryPartName, self::SQL_PARTS_DEFAULTS) && method_exists($this, $newMethodName)) { + Deprecation::trigger( + 'doctrine/dbal', + 'TODO', + 'Calling %s() with "%s" is deprecated, call %s() instead.', + __METHOD__, + $queryPartName, + $newMethodName, + ); + + return $this->$newMethodName(); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'TODO', + 'Calling %s() with "%s" is deprecated without replacement.', + __METHOD__, + $queryPartName, + $newMethodName, + ); + $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName]; $this->state = self::STATE_DIRTY; @@ -1366,6 +1419,48 @@ public function resetQueryPart($queryPartName) return $this; } + /** + * Resets the WHERE conditions for the query. + * + * @return $this This QueryBuilder instance. + */ + public function resetWhere(): self + { + $this->sqlParts['where'] = self::SQL_PARTS_DEFAULTS['where']; + + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Resets the grouping for the query. + * + * @return $this This QueryBuilder instance. + */ + public function resetGroupBy(): self + { + $this->sqlParts['groupBy'] = self::SQL_PARTS_DEFAULTS['groupBy']; + + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Resets the HAVING conditions for the query. + * + * @return $this This QueryBuilder instance. + */ + public function resetHaving(): self + { + $this->sqlParts['having'] = self::SQL_PARTS_DEFAULTS['having']; + + $this->state = self::STATE_DIRTY; + + return $this; + } + /** * Resets the ordering for the query. * diff --git a/tests/Query/QueryBuilderTest.php b/tests/Query/QueryBuilderTest.php index 76fe76bdb7f..acd061f7c53 100644 --- a/tests/Query/QueryBuilderTest.php +++ b/tests/Query/QueryBuilderTest.php @@ -609,37 +609,167 @@ public function testSetFirstResult(): void self::assertEquals(10, $qb->getFirstResult()); } - public function testResetQueryPart(): void + private function prepareQueryBuilderToReset(): QueryBuilder { - $qb = new QueryBuilder($this->conn); + $qb = (new QueryBuilder($this->conn)) + ->select('u.*') + ->distinct() + ->from('users', 'u') + ->where('u.name = ?') + ->orderBy('u.name', 'ASC'); - $qb->select('u.*')->from('users', 'u')->where('u.name = ?'); + self::assertEquals('SELECT DISTINCT u.* FROM users u WHERE u.name = ? ORDER BY u.name ASC', (string) $qb); - self::assertEquals('SELECT u.* FROM users u WHERE u.name = ?', (string) $qb); - $qb->resetQueryPart('where'); - self::assertEquals('SELECT u.* FROM users u', (string) $qb); + return $qb; } public function testResetQueryParts(): void { - $qb = new QueryBuilder($this->conn); + $qb = $this->prepareQueryBuilderToReset(); - $qb->select('u.*')->from('users', 'u')->where('u.name = ?')->orderBy('u.name'); + $this->expectDeprecationWithIdentifier('TODO'); + $qb->resetQueryParts(['distinct', 'where', 'orderBy']); - self::assertEquals('SELECT u.* FROM users u WHERE u.name = ? ORDER BY u.name ASC', (string) $qb); - $qb->resetQueryParts(['where', 'orderBy']); self::assertEquals('SELECT u.* FROM users u', (string) $qb); } - public function testResetOrderBy(): void + public function testLegacyResetSelect(): void { - $qb = new QueryBuilder($this->conn); + $qb = $this->prepareQueryBuilderToReset(); + + $this->expectDeprecationWithIdentifier('TODO'); + $qb->resetQueryPart('select')->addSelect('u.name'); + + self::assertEquals('SELECT DISTINCT u.name FROM users u WHERE u.name = ? ORDER BY u.name ASC', (string) $qb); + } + + public function testLegacyResetDistinct(): void + { + $qb = $this->prepareQueryBuilderToReset(); - $qb->select('u.*')->from('users', 'u')->orderBy('u.name'); + $this->expectDeprecationWithIdentifier('TODO'); + $qb->resetQueryPart('distinct'); + + self::assertEquals('SELECT u.* FROM users u WHERE u.name = ? ORDER BY u.name ASC', (string) $qb); + } - self::assertEquals('SELECT u.* FROM users u ORDER BY u.name ASC', (string) $qb); + public function testResetDistinct(): void + { + $qb = $this->prepareQueryBuilderToReset(); + + $this->expectNoDeprecationWithIdentifier('TODO'); + $qb->distinct(false); + + self::assertEquals('SELECT u.* FROM users u WHERE u.name = ? ORDER BY u.name ASC', (string) $qb); + } + + public function testLegacyResetWhere(): void + { + $qb = $this->prepareQueryBuilderToReset(); + + $this->expectDeprecationWithIdentifier('TODO'); + $qb->resetQueryPart('where'); + + self::assertEquals('SELECT DISTINCT u.* FROM users u ORDER BY u.name ASC', (string) $qb); + } + + public function testResetWhere(): void + { + $qb = $this->prepareQueryBuilderToReset(); + + $this->expectNoDeprecationWithIdentifier('TODO'); + $qb->resetWhere(); + + self::assertEquals('SELECT DISTINCT u.* FROM users u ORDER BY u.name ASC', (string) $qb); + } + + public function testLegacyResetOrderBy(): void + { + $qb = $this->prepareQueryBuilderToReset(); + + $this->expectDeprecationWithIdentifier('TODO'); + $qb->resetQueryPart('orderBy'); + + self::assertEquals('SELECT DISTINCT u.* FROM users u WHERE u.name = ?', (string) $qb); + } + + public function testResetOrderBy(): void + { + $qb = $this->prepareQueryBuilderToReset(); + + $this->expectNoDeprecationWithIdentifier('TODO'); $qb->resetOrderBy(); - self::assertEquals('SELECT u.* FROM users u', (string) $qb); + + self::assertEquals('SELECT DISTINCT u.* FROM users u WHERE u.name = ?', (string) $qb); + } + + private function prepareGroupedQueryBuilderToReset(): QueryBuilder + { + $qb = (new QueryBuilder($this->conn)) + ->select('u.country', 'COUNT(*)') + ->from('users', 'u') + ->groupBy('u.country') + ->having('COUNT(*) > ?') + ->orderBy('COUNT(*)', 'DESC'); + + self::assertEquals( + 'SELECT u.country, COUNT(*) FROM users u GROUP BY u.country HAVING COUNT(*) > ? ORDER BY COUNT(*) DESC', + (string) $qb, + ); + + return $qb; + } + + public function testLegacyResetHaving(): void + { + $qb = $this->prepareGroupedQueryBuilderToReset(); + + $this->expectDeprecationWithIdentifier('TODO'); + $qb->resetQueryPart('having'); + + self::assertEquals( + 'SELECT u.country, COUNT(*) FROM users u GROUP BY u.country ORDER BY COUNT(*) DESC', + (string) $qb, + ); + } + + public function testResetHaving(): void + { + $qb = $this->prepareGroupedQueryBuilderToReset(); + + $this->expectNoDeprecationWithIdentifier('TODO'); + $qb->resetHaving(); + + self::assertEquals( + 'SELECT u.country, COUNT(*) FROM users u GROUP BY u.country ORDER BY COUNT(*) DESC', + (string) $qb, + ); + } + + public function testLegacyResetGroupBy(): void + { + $qb = $this->prepareGroupedQueryBuilderToReset(); + + $this->expectDeprecationWithIdentifier('TODO'); + $qb->resetQueryPart('groupBy'); + + self::assertEquals( + 'SELECT u.country, COUNT(*) FROM users u HAVING COUNT(*) > ? ORDER BY COUNT(*) DESC', + (string) $qb, + ); + } + + public function testGroupBy(): void + { + $qb = $this->prepareGroupedQueryBuilderToReset(); + + $this->expectNoDeprecationWithIdentifier('TODO'); + $qb->resetGroupBy(); + + self::assertEquals( + 'SELECT u.country, COUNT(*) FROM users u HAVING COUNT(*) > ? ORDER BY COUNT(*) DESC', + (string) $qb, + ); } public function testCreateNamedParameter(): void