PHPackages                             welshdev/doctrix - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. welshdev/doctrix

ActiveLibrary[Database &amp; ORM](/categories/database)

welshdev/doctrix
================

A powerful, flexible query builder library for Doctrine ORM with enhanced array-based criteria, fluent interfaces, and built-in pagination

v0.1.7(7mo ago)0539MITPHPPHP &gt;=8.1

Since Aug 10Pushed 7mo ago1 watchersCompare

[ Source](https://github.com/WelshDev/Doctrix)[ Packagist](https://packagist.org/packages/welshdev/doctrix)[ Docs](https://github.com/WelshDev/Doctrix)[ RSS](/packages/welshdev-doctrix/feed)WikiDiscussions master Synced today

READMEChangelogDependencies (8)Versions (9)Used By (0)

Doctrix
=======

[](#doctrix)

A powerful, flexible query builder library for Doctrine ORM that provides an enhanced array-based criteria system and modern fluent interfaces for building complex queries.

Features
--------

[](#features)

### Core Query Features

[](#core-query-features)

- ✅ **Hybrid Approach** - Use via inheritance OR as a service
- ✅ **Clean API** - Simple `fetch()` and `fetchOne()` methods
- ✅ **Fluent Interface** - Modern, chainable API for building queries
- ✅ **Advanced Operators** - Support for `gte`, `lte`, `like`, `contains`, `between`, etc.
- ✅ **Automatic Joins** - Detects and applies joins from dot notation
- ✅ **Filter Functions** - Reusable, named filters
- ✅ **Global Scopes** - Automatically applied filters (like soft deletes)

### Data Operations

[](#data-operations)

- ✅ **Pagination** - Built-in pagination with metadata and cursor support
- ✅ **Bulk Operations** - Efficient `bulkUpdate()` and `bulkDelete()` without fetching entities
- ✅ **Aggregations** - `count()`, `sum()`, `avg()`, `max()`, `min()`
- ✅ **Query Caching** - Built-in result caching support

### Error Handling &amp; Recovery

[](#error-handling--recovery)

- ✅ **Fetch or Fail** - `fetchOneOrFail()` with configurable exceptions (throw 404s directly)
- ✅ **Fetch or Create** - `fetchOneOrCreate()` and `updateOrCreate()` patterns
- ✅ **Sole Results** - `sole()` ensures exactly one result

### Memory-Efficient Processing

[](#memory-efficient-processing)

- ✅ **Chunk Processing** - Process large datasets with `chunk()` and `each()`
- ✅ **Lazy Loading** - Generator-based iteration with `lazy()`
- ✅ **Batch Processing** - Transaction-wrapped batches with `batchProcess()`
- ✅ **Data Transformation** - Transform entities with `map()`

### Existence &amp; Counting

[](#existence--counting)

- ✅ **Existence Checks** - `exists()`, `doesntExist()`, `isEmpty()`
- ✅ **Count Checks** - `hasExactly()`, `hasAtLeast()`, `hasAtMost()`, `hasBetween()`
- ✅ **Optimized Counting** - Check existence without fetching entities

### Random Selection

[](#random-selection)

- ✅ **Random Entities** - `random()` and `randomWhere()` with database-specific optimization
- ✅ **Weighted Random** - `weightedRandom()` for biased selection
- ✅ **Random Distinct** - `randomDistinct()` for unique values

### Relationship Management

[](#relationship-management)

- ✅ **Relationship Checks** - `has()`, `doesntHave()`, `hasCount()`
- ✅ **Complex Conditions** - `whereHas()`, `whereRelation()` with nested criteria
- ✅ **Eager Loading** - `withRelations()` to prevent N+1 queries
- ✅ **Relationship Counts** - `withCount()` for efficient counting

### Data Validation &amp; Integrity

[](#data-validation--integrity)

- ✅ **Uniqueness Checks** - `isUnique()`, `ensureUnique()`, `isUniqueCombination()`
- ✅ **Duplicate Detection** - `fetchDuplicates()` finds duplicate entries
- ✅ **Duplicate Removal** - `removeDuplicates()` with keep strategies
- ✅ **Entity Validation** - `validate()` with rule-based validation

### Advanced Features

[](#advanced-features)

- ✅ **Persistent Filters** - Filters that survive across paginate() and other operations
- ✅ **Request Queries** - Build secure queries from HTTP requests with validation
- ✅ **Macros** - Register reusable custom query methods
- ✅ **Query Debugging** - See SQL, parameters, execution plan, and timing
- ✅ **Works with ANY Repository** - Enhance existing/legacy repositories

Installation
------------

[](#installation)

Install via Composer:

```
composer require welshdev/doctrix
```

Quick Start
-----------

[](#quick-start)

### Option 1: Inheritance (Simple &amp; Clean)

[](#option-1-inheritance-simple--clean)

```
use WelshDev\Doctrix\BaseRepository;

class UserRepository extends BaseRepository
{
    protected string $alias = 'u';
}

// Usage
$users = $userRepo->fetch(['status' => 'active']);
$paginated = $userRepo->paginate(['role' => 'admin'], 1, 20);
```

### Option 2: Service Approach (Flexible &amp; Testable)

[](#option-2-service-approach-flexible--testable)

```
use WelshDev\Doctrix\Service\QueryBuilderService;

class MyController
{
    public function index(QueryBuilderService $queryBuilder)
    {
        // Enhance any repository
        $enhanced = $queryBuilder->for(User::class);

        // Same API as inheritance approach!
        $users = $enhanced->fetch(['status' => 'active']);
        $paginated = $enhanced->paginate(['role' => 'admin'], 1, 20);
    }
}
```

Basic Usage
-----------

[](#basic-usage)

### Array-Based Criteria

[](#array-based-criteria)

```
// Simple criteria
$users = $repo->fetch(['status' => 'active']);

// Complex criteria with operators
$projects = $repo->fetch([
    'status' => 'active',
    ['priority', 'gte', 5],
    ['deadline', 'between', new DateTime('now'), new DateTime('+30 days')],
    ['or', [
        'urgent' => true,
        'priority' => 10
    ]]
]);

// With ordering and limits
$results = $repo->fetch(
    ['category' => 'important'],
    ['created' => 'DESC'],
    10,  // limit
    0    // offset
);
```

### Fluent Interface

[](#fluent-interface)

```
$users = $repo->query()
    ->where('status', 'active')
    ->where('age', '>=', 18)
    ->whereIn('role', ['admin', 'moderator'])
    ->orderBy('created', 'DESC')
    ->paginate(1, 20);
```

### Pagination

[](#pagination)

```
// Full pagination with metadata
$result = $repo->paginate(
    criteria: ['status' => 'active'],
    page: 1,
    perPage: 20
);

echo $result->total;      // Total items
echo $result->lastPage;   // Total pages
echo $result->hasMore;    // Has next page?

// Simple pagination (for infinite scroll)
$simple = $repo->simplePaginate(['status' => 'active'], 1, 20);
// Returns: ['items' => [...], 'hasMore' => true/false]
```

Available Operators
-------------------

[](#available-operators)

### Comparison

[](#comparison)

- `=`, `eq` - Equals
- `!=`, `neq` - Not equals
- `=`, `gte` - Greater than or equal

### Text

[](#text)

- `like` - SQL LIKE
- `not_like` - SQL NOT LIKE
- `contains` - Contains substring
- `starts_with` - Starts with string
- `ends_with` - Ends with string

### Null Checks

[](#null-checks)

- `is_null` - Check for NULL
- `is_not_null` - Check for NOT NULL

### Collections

[](#collections)

- `in` - IN clause
- `not_in` - NOT IN clause
- `between` - BETWEEN clause
- `not_between` - NOT BETWEEN clause

### Logical

[](#logical)

- `or` - OR conditions
- `and` - AND conditions (default)
- `not` - NOT condition

Advanced Features
-----------------

[](#advanced-features-1)

### Named Filters (One-Time Application)

[](#named-filters-one-time-application)

Named filters are pre-defined, reusable query modifications that are applied once per query:

```
class UserRepository extends BaseRepository
{
    protected function defineFilters(): array
    {
        return [
            'active' => fn($qb) => $qb->andWhere('u.status = :status')
                ->setParameter('status', 'active'),
            'verified' => fn($qb) => $qb->andWhere('u.emailVerified = true'),
        ];
    }
}

// Usage - filters are applied once to this specific query
$users = $repo->query()
    ->applyFilter('active')
    ->applyFilter('verified')
    ->get();
```

**Note:** Named filters are cleared after each query execution. For filters that need to persist across operations like pagination (where `count()` and `fetch()` are called separately), use Persistent Filters instead (see below).

### Global Scopes

[](#global-scopes)

```
class PostRepository extends BaseRepository
{
    protected function globalScopes(): array
    {
        return [
            'published' => fn($qb) => $qb->andWhere('p.published = true'),
            'not_deleted' => fn($qb) => $qb->andWhere('p.deletedAt IS NULL'),
        ];
    }
}

// Automatically excludes unpublished and deleted posts
$posts = $repo->fetch();

// Bypass specific scope
$allPosts = $repo->query()
    ->withoutGlobalScope('published')
    ->get();
```

### Query Caching

[](#query-caching)

```
$users = $repo->query()
    ->where('status', 'active')
    ->cache(3600)  // Cache for 1 hour
    ->get();
```

### Aggregations

[](#aggregations)

```
$count = $repo->query()->where('status', 'active')->count();
$sum = $repo->query()->sum('amount');
$avg = $repo->query()->avg('rating');
$max = $repo->query()->max('score');
$min = $repo->query()->min('price');
```

### Bulk Operations

[](#bulk-operations)

```
// Bulk update - deactivate inactive users
$affected = $repo->bulkUpdate(
    ['status' => 'inactive'],
    [['lastLogin', 'lt', new DateTime('-6 months')]]
);

// Bulk delete - remove expired sessions
$deleted = $repo->bulkDelete([
    ['expiresAt', 'lt', new DateTime()]
]);

// Conditional bulk update - only if less than 100 records
$repo->conditionalBulkUpdate(
    ['status' => 'archived'],
    ['status' => 'old'],
    fn($count) => $count < 100
);

// Safe bulk delete with dry run
$result = $repo->safeBulkDelete(['status' => 'expired'], true);
echo "Would delete {$result['count']} records\n";

// Process large dataset in batches
$total = $repo->bulkBatch(
    'update',
    ['status' => 'pending'],
    ['status' => 'processing'],
    500  // Batch size
);
```

### Persistent Filters

[](#persistent-filters)

Persistent filters allow you to apply filters that remain active across multiple query operations, especially useful with pagination where `count()` and `fetch()` are called separately.

```
// Define a repository with persistent filters
class EmailRepository extends BaseRepository
{
    protected string $alias = 'e';

    // Create a filter method that returns a cloned instance
    public function filterByUser(User $user): self
    {
        return $this->withFilter('user', $user);
    }

    // Define how the filter is applied to queries
    protected function applyUserFilter(QueryBuilder $qb, User $user): void
    {
        $qb->andWhere('e.user = :user')
           ->setParameter('user', $user);
    }
}

// Usage - filter persists across pagination
$emails = $repo->filterByUser($currentUser)
    ->paginate(page: 1, perPage: 20);

// Chain multiple filters
$emails = $repo
    ->withFilter('status', 'sent')
    ->withFilter('priority', 'high')
    ->fetch();

// Remove filters
$repo = $repo->withoutFilter('status');

// Check if filter is active
if ($repo->hasFilter('user')) {
    // Filter is active
}
```

#### Convention-Based Filter Application

[](#convention-based-filter-application)

The `PersistentFiltersTrait` uses a naming convention to automatically find and apply filter methods:

1. Call `withFilter('filterName', $value)` to register a filter
2. Define `applyFilterNameFilter(QueryBuilder $qb, $value)` to handle the filter
3. The trait automatically calls your method when building queries

```
class ProductRepository extends BaseRepository
{
    // Register multiple filters at once
    public function applyFilters(array $filters): self
    {
        return $this->withFilters($filters);
    }

    // Each filter has its own apply method
    protected function applyCategoryFilter(QueryBuilder $qb, Category $category): void
    {
        $qb->andWhere('p.category = :category')
           ->setParameter('category', $category);
    }

    protected function applyPriceRangeFilter(QueryBuilder $qb, array $range): void
    {
        $qb->andWhere('p.price BETWEEN :min AND :max')
           ->setParameter('min', $range['min'])
           ->setParameter('max', $range['max']);
    }
}

// Usage
$products = $repo->applyFilters([
    'category' => $electronics,
    'priceRange' => ['min' => 100, 'max' => 500]
])->paginate(1, 20);
```

#### Complex Filter Example

[](#complex-filter-example)

```
class EmailRepository extends BaseRepository
{
    public function filterByOperative(Operative $operative): self
    {
        return $this->withFilter('operative', $operative);
    }

    protected function applyOperativeFilter(QueryBuilder $qb, Operative $operative): void
    {
        // Complex joins for inheritance hierarchy
        $qb->leftJoin(OperativeEmail::class, 'oe', 'WITH', 'e.id = oe.id')
           ->leftJoin(ContractEmail::class, 'ce', 'WITH', 'e.id = ce.id')
           ->leftJoin('ce.contract', 'c')
           ->andWhere(
               $qb->expr()->orX(
                   'oe.operative = :operative',
                   'c.operative = :operative'
               )
           )
           ->setParameter('operative', $operative);
    }
}
```

### Macros (Custom Query Methods)

[](#macros-custom-query-methods)

```
// Register reusable query methods
$repo->registerMacro('activeAdmins', function($query) {
    return $query
        ->where('status', 'active')
        ->where('role', 'admin');
});

// Register multiple macros
$repo->registerMacros([
    'verified' => fn($q) => $q->where('emailVerified', true),
    'premium' => fn($q) => $q->where('subscriptionType', 'premium'),
    'recent' => fn($q) => $q->where('createdAt', '>=', new DateTime('-30 days'))
]);

// Use macros in queries
$admins = $repo->query()->activeAdmins()->get();

// Chain multiple macros
$users = $repo->query()
    ->verified()
    ->premium()
    ->recent()
    ->paginate(1, 20);

// Parameterized macros
$repo->registerMacro('olderThan', function($query, $days) {
    return $query->where('createdAt', '', 5)->fetch()              // Count condition
$repo->doesntHave('orders')->fetch()               // Missing relationship
$repo->whereHas('orders', 'status', 'pending')     // Filter by related
$repo->withRelations(['orders', 'profile'])        // Eager load
```

### Data Validation

[](#data-validation)

```
$repo->isUnique('email', $value, $excludeId)       // Check uniqueness
$repo->ensureUnique('email', $value)               // Throw if not unique
$repo->fetchDuplicates(['email'])                  // Find duplicates
$repo->removeDuplicates(['email'], 'first')        // Remove duplicates
```

### Random Selection

[](#random-selection-1)

```
$repo->random($count)                               // Random entities
$repo->randomWhere($criteria, $count)              // Random with criteria
$repo->weightedRandom(['priority' => 'ASC'], 5)    // Weighted selection
```

Using Traits
------------

[](#using-traits)

Mix and match traits for additional functionality:

```
use WelshDev\Doctrix\BaseRepository;
use WelshDev\Doctrix\Traits\CacheableTrait;
use WelshDev\Doctrix\Traits\SoftDeleteTrait;

class ProductRepository extends BaseRepository
{
    use CacheableTrait;
    use SoftDeleteTrait;

    protected string $alias = 'p';
    protected string $softDeleteField = 'deletedAt';
}

// Automatically excludes soft-deleted records
$products = $repo->fetch();

// Include soft-deleted records
$allProducts = $repo->fetchWithDeleted();

// Only soft-deleted records
$deletedProducts = $repo->fetchOnlyDeleted();
```

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance65

Regular maintenance activity

Popularity17

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 84.6% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~16 days

Recently: every ~28 days

Total

8

Last Release

215d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/63b9ae74c44591eb25a554c9f687ceef538dad518fc65f818dd7d62841c41230?d=identicon)[WelshDev](/maintainers/WelshDev)

---

Top Contributors

[![WelshDev](https://avatars.githubusercontent.com/u/57954883?v=4)](https://github.com/WelshDev "WelshDev (11 commits)")[![Copilot](https://avatars.githubusercontent.com/in/1143301?v=4)](https://github.com/Copilot "Copilot (2 commits)")

---

Tags

symfonydatabaseormdoctrinedbalpaginationquery builderrepository

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/welshdev-doctrix/health.svg)

```
[![Health](https://phpackages.com/badges/welshdev-doctrix/health.svg)](https://phpackages.com/packages/welshdev-doctrix)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M386](/packages/easycorp-easyadmin-bundle)[doctrine/doctrine-bundle

Symfony DoctrineBundle

4.8k254.4M4.1k](/packages/doctrine-doctrine-bundle)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M738](/packages/sylius-sylius)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.8M508](/packages/pimcore-pimcore)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1616.4k14](/packages/2lenet-crudit-bundle)[laravel-doctrine/orm

An integration library for Laravel and Doctrine ORM

8465.5M96](/packages/laravel-doctrine-orm)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
