ecommit/doctrine-utils
Small set of Doctrine ORM QueryBuilder utilities: accurate COUNT helpers, a paginator, and filter helper methods. Install via Composer and use to simplify common query building patterns in your PHP projects.
Installation:
composer require ecommit/doctrine-utils
Ensure your Laravel project uses Doctrine DBAL or Doctrine ORM (via doctrine/dbal or doctrine/orm packages).
First Use Case: Counting Results (ORM Example):
use Ecommit\DoctrineUtils\Paginator\DoctrinePaginatorBuilder;
use Doctrine\ORM\EntityManagerInterface;
$queryBuilder = $entityManager->getRepository(Product::class)->createQueryBuilder('p');
$count = DoctrinePaginatorBuilder::countQueryBuilder([
'query_builder' => $queryBuilder,
'behavior' => 'orm' // Default, leverages Doctrine's native counting
]);
Pagination (DBAL Example):
use Ecommit\DoctrineUtils\Paginator\DoctrineDBALPaginator;
use Doctrine\DBAL\Query\QueryBuilder;
$queryBuilder = $connection->createQueryBuilder();
$paginator = new DoctrineDBALPaginator([
'query_builder' => $queryBuilder,
'page' => 1,
'max_per_page' => 20
]);
$results = $paginator->getResults();
Where to Look First:
count.md and paginator.md for core use cases.filters.md for dynamic query filtering (e.g., QueryBuilderFilter).PaginatorInterface for consistent pagination methods.ORM Integration:
// In a Laravel controller/service
public function index(EntityManagerInterface $em, Request $request)
{
$qb = $em->getRepository(Product::class)->createQueryBuilder('p');
$paginator = new DoctrineORMPaginator([
'query_builder' => $qb,
'page' => (int) $request->query('page', 1),
'max_per_page' => 15,
'count' => ['behavior' => 'orm'] // Auto-counts results
]);
return view('products.index', [
'products' => $paginator->getResults(),
'paginator' => $paginator // For pagination links
]);
}
DBAL Integration:
public function listUsers(Connection $connection, Request $request)
{
$qb = $connection->createQueryBuilder();
$paginator = new DoctrineDBALPaginator([
'query_builder' => $qb,
'page' => $request->query('page', 1),
'max_per_page' => 10,
'by_identifier' => 'id' // Optimize for large datasets
]);
return $paginator->getResults();
}
Use QueryBuilderFilter for reusable conditions:
use Ecommit\DoctrineUtils\QueryBuilderFilter;
$filter = new QueryBuilderFilter($qb);
$filter->add('p.price > :min', ['min' => 100]); // Add WHERE clause
$filter->add('p.category = :category', ['category' => 'Electronics']);
Laravel Service Container: Bind the filter to Laravel’s IoC:
// config/app.php
'bindings' => [
Ecommit\DoctrineUtils\QueryBuilderFilter::class => function ($app) {
return new QueryBuilderFilter($app->make(QueryBuilder::class));
},
];
by_identifier: For large datasets, use by_identifier to fetch only IDs first, then hydrate:
$paginator = new DoctrineORMPaginator([
'query_builder' => $qb,
'by_identifier' => 'id' // Reduces memory usage
]);
ORDER BY for counts:
$count = DoctrinePaginatorBuilder::countQueryBuilder([
'query_builder' => $qb,
'behavior' => 'orm',
'simplified_request' => true
]);
Bridge Doctrine ORM with Eloquent (if needed):
// Convert Eloquent Query Builder to Doctrine ORM QB
$doctrineQb = $entityManager->getConnection()->getQueryBuilder();
$eloquentQb = Product::query()->getQuery();
// Manually translate conditions (e.g., using QueryBuilderFilter)
ORM vs. DBAL Confusion:
DoctrineDBALPaginator with an ORM QueryBuilder.DoctrineORMPaginator for ORM).Counting Behavior Quirks:
count_by_sub_request requires a connection parameter but may fail silently.connection is properly injected:
$count = DoctrinePaginatorBuilder::countQueryBuilder([
'query_builder' => $qb,
'behavior' => 'count_by_sub_request',
'connection' => $entityManager->getConnection() // Explicitly pass
]);
by_identifier Limitations:
DISTINCT ON (PostgreSQL) or custom logic.Laravel Caching:
Route::get('/products', function () {
return Cache::remember('products_page_1', now()->addHours(1), function () {
return $paginator->getResults();
});
});
Log QueryBuilders:
$qb->getSQL(); // Inspect raw SQL
$qb->getParameters(); // Check bound parameters
Enable Doctrine Logging:
$entityManager->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
Validate Count Options:
alias is provided for count_by_alias:
$count = DoctrinePaginatorBuilder::countQueryBuilder([
'query_builder' => $qb,
'behavior' => 'count_by_alias',
'alias' => 'p' // Must match your query alias
]);
Custom Paginator:
Extend DoctrineORMPaginator to add Laravel-specific features:
class LaravelDoctrinePaginator extends DoctrineORMPaginator
{
public function toJson()
{
return response()->json([
'data' => $this->getResults(),
'meta' => [
'total' => $this->getTotalResults(),
'page' => $this->getCurrentPage()
]
]);
}
}
Filter Chaining:
Create a fluent interface for QueryBuilderFilter:
$filter->where('p.price > :min')->andWhere('p.category = :category');
Hybrid Pagination:
Combine with Laravel’s LengthAwarePaginator:
$items = $paginator->getResults();
$paginated = new LengthAwarePaginator(
$items,
$paginator->getTotalResults(),
$paginator->getMaxPerPage(),
$paginator->getCurrentPage(),
['path' => LaravelPagination::resolveCurrentPath()]
);
Default Values:
max_per_page defaults to 100 (adjust in your paginator constructor).simplified_request is true for ORM unless by_identifier is used.Connection Handling:
connection parameter is passed when using count_by_sub_request.EntityManager connection is auto-resolved.Distinct Behavior:
distinct_alias is true by default for count_by_alias. Set to false for non-distinct counts:
'distinct_alias' => false
How can I help you explore Laravel packages today?