New Doctrine\ORM\QueryInterface to combat "final class Query"; mocking in unit tests #7014

Open
opened 2026-01-22 15:43:11 +01:00 by admin · 4 comments
Owner

Originally created by @kafoso on GitHub (Jul 21, 2022).

New Doctrine\ORM\QueryInterface to combat final class Query

Q A
New Feature yes
RFC no
BC Break no, except in specific cases

Summary

As discussed in https://github.com/doctrine/orm/issues/2830, the class Doctrine\ORM\Query is final and it continues to be in 3.0.x: d41c4c6cc6/lib/Doctrine/ORM/Query.php

Doctrine\ORM\Query extends Doctrine\ORM\AbstractQuery, but that is the end of the road. No other classes, interfaces, or traits are involved.

This is a hassle in the following scenarios:

  1. You cannot implement a decorator/wrapper for Doctrine\ORM\Query.
  2. When working with unit tests, you cannot mock the Doctrine\ORM\Query class.
Proposal
  1. Introduce a new Doctrine\ORM\QueryInterface, which Doctrine\ORM\Query (or Doctrine\ORM\AbstractQuery) then implements.
  2. Change methods, which type hint Doctrine\ORM\Query (parameters and return types) to instead type hint Doctrine\ORM\QueryInterface.
  3. Optional: Move documentation (docblocks) from Doctrine\ORM\Query (and potentially Doctrine\ORM\AbstractQuery) to Doctrine\ORM\QueryInterface.

Regarding point 2 above: This may cause backwards compatibility issues in cases where userland code type hints Doctrine\ORM\Query. This should be a fairly limited corner case, again because Doctrine\ORM\Query is final, and the very absence of an interface prevents decorator implementations.

Originally created by @kafoso on GitHub (Jul 21, 2022). ### New `Doctrine\ORM\QueryInterface` to combat `final class Query` <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |------------ | ------ | New Feature | yes | RFC | no | BC Break | no, except in specific cases #### Summary <!-- Provide a summary of the feature you would like to see implemented. --> As discussed in https://github.com/doctrine/orm/issues/2830, the class `Doctrine\ORM\Query` is final and it continues to be in 3.0.x: https://github.com/doctrine/orm/blob/d41c4c6cc6216dafd0fea7382a9f791476cdb039/lib/Doctrine/ORM/Query.php `Doctrine\ORM\Query` extends `Doctrine\ORM\AbstractQuery`, but that is the end of the road. No other classes, interfaces, or traits are involved. This is a hassle in the following scenarios: 1. You cannot implement a decorator/wrapper for `Doctrine\ORM\Query`. 2. When working with unit tests, you cannot mock the `Doctrine\ORM\Query` class. ##### Proposal 1. Introduce a new `Doctrine\ORM\QueryInterface`, which `Doctrine\ORM\Query` (or `Doctrine\ORM\AbstractQuery`) then implements. 3. Change methods, which type hint `Doctrine\ORM\Query` (parameters and return types) to instead type hint `Doctrine\ORM\QueryInterface`. 4. Optional: Move documentation (docblocks) from `Doctrine\ORM\Query` (and potentially `Doctrine\ORM\AbstractQuery`) to `Doctrine\ORM\QueryInterface`. Regarding point 2 above: This _may_ cause backwards compatibility issues in cases where userland code type hints `Doctrine\ORM\Query`. This should be a fairly limited corner case, again because `Doctrine\ORM\Query` is final, and the very absence of an interface prevents decorator implementations.
Author
Owner

@kafoso commented on GitHub (Jul 23, 2022):

I've made the code changes necessary to achieve the above and said changes are fairly manageable. Both phpstan and phpunit are still happy.

I moved the Doctrine\ORM\AbstractQuery::HYDRATE_* constants to the new Doctrine\ORM\QueryInterface, and because of this, I:

  1. Updated documentation references in various places.
  2. Made a test (tests/Doctrine/Tests/ORM/Functional/Ticket/GH9926Test.php), which specifically tests, that Doctrine\ORM\AbstractQuery::HYDRATE_* references continue to function. This is mainly to ensure userland implementations targeting these constants won't break.

If desired, I'll make a pull request with said changes.

I did, however, notice that a good number of classes in this library are final. Arguments for having all these classes be final probably exist, but the above would be completely obsolete, if the "final" keyword was removed from Doctrine\ORM\Query and potentially other classes.

It is still a good idea to cover central classes with an interface, though. This leads to another question: Should we look at other classes, which suffer from the same "final class" restriction, and also provide an interface for these classes?

@kafoso commented on GitHub (Jul 23, 2022): I've made the code changes necessary to achieve the above and said changes are fairly manageable. Both phpstan and phpunit are still happy. I moved the `Doctrine\ORM\AbstractQuery::HYDRATE_*` constants to the new `Doctrine\ORM\QueryInterface`, and because of this, I: 1. Updated documentation references in various places. 2. Made a test (`tests/Doctrine/Tests/ORM/Functional/Ticket/GH9926Test.php`), which specifically tests, that `Doctrine\ORM\AbstractQuery::HYDRATE_*` references continue to function. This is mainly to ensure userland implementations targeting these constants won't break. If desired, I'll make a pull request with said changes. I did, however, notice that a good number of classes in this library are **final**. Arguments for having all these classes be final probably exist, but the above would be completely obsolete, if the "final" keyword was removed from `Doctrine\ORM\Query` and potentially other classes. It is still a good idea to cover central classes with an interface, though. This leads to another question: Should we look at other classes, which suffer from the same "final class" restriction, and also provide an interface for these classes?
Author
Owner

@beberlei commented on GitHub (Jul 23, 2022):

If you feel that you want to mock Query, then you sre doing testing wrong imho.

First, you should never mock apis that you dont own.

Second, returning mocks from mocks is usually sign of a code smell. Since you need to mock either EM or Repository to get to a query.

You should wrap the query in a repository and use an integration test.

@beberlei commented on GitHub (Jul 23, 2022): If you feel that you want to mock Query, then you sre doing testing wrong imho. First, you should never mock apis that you dont own. Second, returning mocks from mocks is usually sign of a code smell. Since you need to mock either EM or Repository to get to a query. You should wrap the query in a repository and use an integration test.
Author
Owner

@kafoso commented on GitHub (Jul 23, 2022):

At https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/query-builder.html (latest currently being 3.3.7), the authors have entirely omitted an explanation of how results are retrieved using the QueryBuilder. Perhaps an oversight?

If we turn back the clock and look at https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/query-builder.html instead, we see the following:

A QueryBuilder provides an API that is designed for conditionally constructing a DQL query in several steps.

And further down (Executing a Query), we see:

The QueryBuilder is a builder object only - it has no means of actually executing the Query. ... This is why you always have to convert a querybuilder instance into a Query object:

The code example below on the website specifically mentions Doctrine\ORM\QueryBuilder->getQuery().

Looking at the 3.0.x code for the QueryBuilder, it does not appear this API has changed. I.e. we will still have to do Doctrine\ORM\QueryBuilder->getQuery().

Surely, this means Doctrine\ORM\Query is part of the public API when using query builders?

To be clear, I am speaking mostly of decorator and testing scenarios in userland code.

If tests are written for e.g. repositories, which has access to the Entity Manager (Doctrine\ORM\EntityManagerInterface), and Doctrine\ORM\EntityManagerInterface->createQueryBuilder() is called in the repository class, the test code should be able to control the resulting Query instance. For unit testing purposes, this would be a mock (e.g. PHPUnit\Framework\MockObject\MockObject when using phpunit, which – despite the name – is an interface).

Mocking classes or interfaces is how most unit tests are performed, providing safeguards against unwanted and unintended code execution. It is not code smell. It literally is a major feature of phpunit and other xUnit implementations.

Unit tests over Integration tests. Otherwise, you'll be forcing userland implementations to deal with even more of the internals of Doctrine and potentially interface it with a database, which goes even further counter to the simple fix I am proposing here.

Don't get me wrong: Integration tests most definitely should be performed as part of the Doctrine test suite. Rarely is this necessary for the userland implementations.

The final keyword isn't inherently bad (as Jon Skeet would probably attest to), which is why I am not suggesting removing it. I am instead suggesting the interface alternative, which (A) keeps the Doctrine\ORM\Query class "under lock and key", but (B) allows for decorator patterns, mocking, and ultimately entire substitutions in userland (if people want to go crazy, they will).

@kafoso commented on GitHub (Jul 23, 2022): At https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/query-builder.html (latest currently being 3.3.7), the authors have entirely omitted an explanation of how results are retrieved using the QueryBuilder. Perhaps an oversight? If we turn back the clock and look at https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/query-builder.html instead, we see the following: > A QueryBuilder provides an API that is designed for conditionally constructing a DQL query in several steps. And further down ([Executing a Query](https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/query-builder.html#executing-a-query)), we see: > The QueryBuilder is a builder object only - it has no means of actually executing the Query. ... This is why you always have to convert a querybuilder instance into a Query object: The code example below on the website specifically mentions `Doctrine\ORM\QueryBuilder->getQuery()`. Looking at the [3.0.x code for the QueryBuilder](https://github.com/doctrine/orm/blob/3.0.x/lib/Doctrine/ORM/QueryBuilder.php), it does not appear this API has changed. I.e. we will still have to do `Doctrine\ORM\QueryBuilder->getQuery()`. Surely, this means `Doctrine\ORM\Query` _is_ part of the public API when using query builders? To be clear, I am speaking mostly of decorator and testing scenarios in userland code. If tests are written for e.g. repositories, which has access to the Entity Manager (`Doctrine\ORM\EntityManagerInterface`), and `Doctrine\ORM\EntityManagerInterface->createQueryBuilder()` is called in the repository class, the test code should be able to control the resulting Query instance. For unit testing purposes, this would be a mock (e.g. `PHPUnit\Framework\MockObject\MockObject` when using phpunit, which – despite the name – is an interface). Mocking classes or interfaces is how most unit tests are performed, providing safeguards against unwanted and unintended code execution. It is not code smell. It literally is a major feature of phpunit and other xUnit implementations. Unit tests over Integration tests. Otherwise, you'll be forcing userland implementations to deal with even more of the internals of Doctrine and potentially interface it with a database, which goes even further counter to the simple fix I am proposing here. Don't get me wrong: Integration tests most definitely should be performed as part of the Doctrine test suite. Rarely is this necessary for the userland implementations. The `final` keyword isn't inherently bad (as Jon Skeet would probably attest to), which is why I am not suggesting removing it. I am instead suggesting the interface alternative, which (A) keeps the `Doctrine\ORM\Query` class "under lock and key", but (B) allows for decorator patterns, mocking, and ultimately entire substitutions in userland (if people want to go crazy, they will).
Author
Owner

@VincentLanglet commented on GitHub (Nov 20, 2023):

If you feel that you want to mock Query, then you sre doing testing wrong imho.

First, you should never mock apis that you dont own.

Second, returning mocks from mocks is usually sign of a code smell. Since you need to mock either EM or Repository to get to a query.

You should wrap the query in a repository and use an integration test.

In my case @beberlei we would like to decorate the Query::execute method which is currently not possible because the class is final without any interface.

@VincentLanglet commented on GitHub (Nov 20, 2023): > If you feel that you want to mock Query, then you sre doing testing wrong imho. > > First, you should never mock apis that you dont own. > > Second, returning mocks from mocks is usually sign of a code smell. Since you need to mock either EM or Repository to get to a query. > > You should wrap the query in a repository and use an integration test. In my case @beberlei we would like to decorate the Query::execute method which is currently not possible because the class is final without any interface.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7014