Search by

pkly / query-helper

pkly

Simple utility class for easier use of Doctrine ORM Queries in certain scenarios

Package info

github.com/pkly/query-helper

pkg:composer/pkly/query-helper

Statistics

Installs: 772

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

1.4.0 2026-11-03 13:53 UTC

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

Packagist Downloads

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.