Skip to content

Check QueryBuilder orderBy() clauses that use the SortDirection enum - #796

Open
whataboutpereira wants to merge 1 commit into
phpstan:2.1.xfrom
whataboutpereira:2.1.x
Open

whataboutpereira wants to merge 1 commit into
phpstan:2.1.xfrom
whataboutpereira:2.1.x

Conversation

@whataboutpereira

Copy link
Copy Markdown
Contributor

Closes #792

Problem

ORM 3.7 accepts \SortDirection as the direction in QueryBuilder::orderBy() / addOrderBy() and Expr\OrderBy, and deprecates the string form. With the enum, QueryBuilderDqlRule reports nothing for an unknown field:

$this->createQueryBuilder('t')
    ->orderBy('t.nonexistent', \SortDirection::Ascending)
    ->getQuery();

ArgumentsProcessor::processArgs() does not understand enum cases, so it throws DynamicQueryBuilderArgumentException. orderBy and addOrderBy are in METHODS_NOT_AFFECTING_RESULT_TYPE, so the call is skipped when the QueryBuilder is replayed. The rebuilt DQL has no ORDER BY and the field is never validated.

Changes

  • ArgumentsProcessor: an argument that is a \SortDirection case is resolved to the real enum instance when the installed ORM is 3.7 or newer. The instance is passed instead of 'ASC'/'DESC' because ORM 3.7 matches on the enum and triggers a deprecation for strings. This also covers new Expr\OrderBy('e.x', \SortDirection::Descending).
  • NewExprDynamicReturnTypeExtension: catches Throwable around the constructor call and falls back to ObjectType.
  • QueryBuilderGetQueryDynamicReturnTypeExtension: catches Throwable around getDQL().

The two catches are needed because a real SortDirection instance can now reach Doctrine code that does not accept it, e.g. new Expr\From(SortDirection::Ascending, 'e') (TypeError) or an Expr\Comparison holding the enum (cannot be converted to a string in getDQL()). Without them PHPStan crashes on such code.

Behaviour

  • ORM < 3.7 and enums other than SortDirection: unchanged.
  • ORM >= 3.7: a SortDirection passed somewhere that does not accept it makes the QueryBuilder not analysable, instead of having that call skipped. Such code already fails at runtime in Doctrine itself (TypeError, or "Object of class SortDirection could not be converted to string" when the DQL is built), so this only changes how PHPStan degrades on code that is already broken.

Tests

QueryBuilderDqlRuleTest::testSortDirection, skipped when \SortDirection does not exist:

  • ORM >= 3.7: unknown fields in orderBy(), addOrderBy() and new Expr\OrderBy() are reported, valid ones are not, and the two misuse cases are reported as not analysable.
  • ORM < 3.7: same results as before this PR.

Verified locally on PHP 8.5:

  • ORM 2.20.13 / DBAL 3.10: full suite, lint and PHPStan pass.
  • ORM 3.7.4 / DBAL 4.5: full suite and phpcs pass; removing either catch makes the new test crash.

Infection was not run locally. The new branch and both catches only execute on ORM 3.7+, so they are not covered by a default (ORM 2) install.

Not included

  • A non-constant direction (SortDirection $dir, string $dir) still causes the orderBy() call to be skipped, as before.

Co-Authored-By: Claude Code

ORM 3.7 accepts the \SortDirection enum as the direction in
QueryBuilder::orderBy() / addOrderBy() and Expr\OrderBy, and
deprecates the string form. ArgumentsProcessor only understood
constant scalars, constant arrays, class-strings and ExprType, so an
enum case was treated as a dynamic argument. Because orderBy and
addOrderBy are in METHODS_NOT_AFFECTING_RESULT_TYPE, the whole call
was then silently dropped from the replayed QueryBuilder, the
resulting DQL had no ORDER BY clause, and unknown fields in it were
never reported.

ArgumentsProcessor now resolves an argument that is a \SortDirection
case to the real enum instance when the installed ORM is 3.7 or
newer. The instance is passed instead of 'ASC'/'DESC' because ORM 3.7
matches on the enum and triggers a deprecation for strings. On older
ORM, and for any other enum, nothing changes.

A SortDirection instance can now reach Doctrine code that does not
accept it, so two replay steps are guarded:

- NewExprDynamicReturnTypeExtension catches Throwable around the
  constructor call and falls back to ObjectType, e.g. for
  new Expr\From(SortDirection::Ascending, 'e').
- QueryBuilderGetQueryDynamicReturnTypeExtension catches Throwable
  around getDQL(), e.g. for an Expr\Comparison holding the enum,
  which cannot be converted to a string.

Such code already fails at runtime in Doctrine itself, so this only
changes how PHPStan degrades on it: the QueryBuilder is treated as
not analysable instead of having that one call skipped.

Closes phpstan#792

Co-Authored-By: Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ORM 3.7 SortDirection support in orderBy clause

1 participant