pkly / query-helper
Simple utility class for easier use of Doctrine ORM Queries in certain scenarios
Requires
- php: ^8.4
- doctrine/dbal: ^4
- doctrine/orm: ^3
Requires (Dev)
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-03 13:54:00 UTC
README
Simple utility class for easier use of Doctrine ORM Queries in certain scenarios
Installation
Simply run
composer require pkly/query-helper
Usage
In your repositories simply return an instance of QueryHelper instead of a full type.
class FooEntityRepository extends \Doctrine\ORM\EntityRepository { /** * @return QueryHelper<FooEntity, int> */ public function findByOrder( Order $order ): QueryHelper { return new QueryHelper( $this->createQueryBuilder('entity') ->where('entity.relatedOrder = :relaterOrder') ->setParameter('relaterOrder', $order) ); } }
Then use it as normal.
// inside of a service, controller, etc. $helper = $repository->findByOrder($order); $helper->ids(); // list of ids $helper->id(); // single id $helper->value(); // single object $helper->list(); // list of objects $helper->reference(); // single reference from Doctrine $helper->references(); // list of references $helper->fields(['order']); // single array of fields (one row) $helper->fieldList(['order']); // list of fields (multiple rows) $helper->model(Model::of(OrderSummary::class, ['id', 'IDENTITY(entity.relatedOrder)'])); // single DTO via SELECT NEW $helper->models(Model::of(OrderSummary::class, ['id', 'IDENTITY(entity.relatedOrder)'])); // list of DTOs via SELECT NEW $helper->count(); // simple count $helper->exists(); // boolean existence check // more advanced use // load one object with lock from query $object = $em->getRepository(FooEntity::class) ->findByOrder($order) ->lockMode(LockMode::PESSIMISTIC_WRITE) ->value(); // returns T|null
DTO hydration
models() / model() take a Model::of(Class::class, [...fields]) and hydrate rows straight into
that class through DQL SELECT NEW, so no entity is loaded and nothing touches the UnitOfWork.
Fields are passed to the constructor in order; bare names are prefixed with entity., anything
containing a dot or function call is used as-is.
final class OrderSummary { public function __construct( private readonly int $id, private readonly int $orderId, ) { } } $repository->findByOrder($order)->models(Model::of(OrderSummary::class, ['id', 'IDENTITY(entity.relatedOrder)']));
A field may itself be a Model, which becomes a nested NEW expression: the inner object is built
first and passed as a constructor argument of the outer one. The joined alias (c below) must exist
on the same query builder.
final class OrderDetails { public function __construct( private readonly int $id, private readonly float $total, private readonly CustomerSummary $customer, // hydrated by the nested NEW ) { } } $helper = $repository->findByOrder($order); $helper->getQueryBuilder()->join('entity.customer', 'c'); $helper->models(Model::of(OrderDetails::class, [ 'id', 'total', Model::of(CustomerSummary::class, ['c.id', 'c.name']), ])); // SELECT NEW OrderDetails(entity.id, entity.total, NEW CustomerSummary(c.id, c.name)) ...
Nesting can go arbitrarily deep (a Model inside a Model inside a Model), still in a single query
and without hydrating any entity. It only covers to-one shapes: each row yields exactly one inner
object per nested Model. A to-many relation cannot be hydrated into a collection this way — joining
one would produce one outer object per joined row instead.
Several sibling Models give side-by-side DTOs per row: wrap them in a class whose constructor takes
each one.
final class OrderWithCustomer { public function __construct( public readonly OrderSummary $order, public readonly CustomerSummary $customer, ) { } } $helper->models(Model::of(OrderWithCustomer::class, [ Model::of(OrderSummary::class, ['id', 'total']), Model::of(CustomerSummary::class, ['c.id', 'c.name']), ])); // Collection<int, OrderWithCustomer>
Feature requests?
Sure, hit me up with an issue if you wish.