PHPackages                             vidic/orm-search-param - 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. vidic/orm-search-param

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

vidic/orm-search-param
======================

PHP 8 attribute-based search parameter parser for Doctrine ORM QueryBuilder

0.1.0(1mo ago)13MITPHPPHP ^8.1CI passing

Since Apr 27Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/vidic-igor/orm-search-param)[ Packagist](https://packagist.org/packages/vidic/orm-search-param)[ RSS](/packages/vidic-orm-search-param/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (1)Dependencies (3)Versions (2)Used By (0)

orm-search-param
================

[](#orm-search-param)

[![PHP Version](https://camo.githubusercontent.com/cc9cdea9aa96b40a822425e981b0a030e3371202973c7d57b74e8e99834f81dc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e312d626c7565)](https://www.php.net/)[![License](https://camo.githubusercontent.com/f5f962c3afa05d7ac72ac8764f2e66e78ab3753c195175cbc475827cbffddd4a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f76696469632d69676f722f6f726d2d7365617263682d706172616d)](LICENSE)[![Tests](https://camo.githubusercontent.com/b801dc7f0f0093670d3de8d7dddf6eb0d6c0be86284daa9a36bf8ea8b4287e20/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f76696469632d69676f722f6f726d2d7365617263682d706172616d2f74657374732e796d6c3f6c6162656c3d7465737473)](https://github.com/vidic-igor/orm-search-param/actions)

PHP 8 attribute-based search parameter parser for Doctrine ORM `QueryBuilder`. Eliminates repetitive `if ($filter->x) { $qb->andWhere(...) }` chains from repository search methods.

Inspired by [nebkam/odm-search-param](https://github.com/nebkam/odm-search-param), which does the same for MongoDB ODM.

---

Before &amp; After
------------------

[](#before--after)

**Before** — 70+ lines of imperative if-chains in every repository:

```
public function search(UserSearchFilter $filter): array
{
    $qb = $this->createQueryBuilder('u');

    if ($filter->name) {
        $qb->andWhere('u.name = :name')->setParameter('name', $filter->name);
    }
    if ($filter->email) {
        $qb->andWhere('u.email LIKE :email')->setParameter('email', '%' . $filter->email . '%');
    }
    if ($filter->role) {
        $qb->andWhere('u.role = :role')->setParameter('role', $filter->role->value);
    }
    if ($filter->createdFrom) {
        $qb->andWhere('u.createdAt >= :createdFrom')->setParameter('createdFrom', $filter->createdFrom);
    }
    // ... 50 more lines

    return $qb->getQuery()->getResult();
}
```

**After** — declare the filter once, call `parse()` in the repository:

```
#[OrmRootAlias('u')]
class UserSearchFilter
{
    #[OrmSearchParam(type: OrmSearchParamType::Equals)]
    public ?string $name = null;

    #[OrmSearchParam(type: OrmSearchParamType::Like)]
    public ?string $email = null;

    #[OrmSearchParam(type: OrmSearchParamType::StringEnum)]
    public ?UserRole $role = null;

    #[OrmSearchParam(type: OrmSearchParamType::Range, direction: OrmSearchParamDirection::From, field: 'createdAt')]
    public ?DateTimeImmutable $createdFrom = null;
}
```

```
public function search(UserSearchFilter $filter): array
{
    $alias = OrmSearchParamParser::getAlias($filter);
    $qb    = $this->createQueryBuilder($alias);
    OrmSearchParamParser::parse($filter, $qb);

    return $qb->getQuery()->getResult();
}
```

---

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

[](#installation)

```
composer require vidic/orm-search-param
```

---

Usage
-----

[](#usage)

### 1. Annotate the filter class

[](#1-annotate-the-filter-class)

Add `#[OrmRootAlias]` to declare the QueryBuilder alias, then `#[OrmSearchParam]` on each property you want to filter by:

```
use Vidic\OrmSearchParam\OrmRootAlias;
use Vidic\OrmSearchParam\OrmSearchParam;
use Vidic\OrmSearchParam\OrmSearchParamType;
use Vidic\OrmSearchParam\OrmSearchParamDirection;

#[OrmRootAlias('u')]
class UserSearchFilter
{
    #[OrmSearchParam(type: OrmSearchParamType::Equals)]
    public ?string $name = null;

    #[OrmSearchParam(type: OrmSearchParamType::Like)]
    public ?string $email = null;

    #[OrmSearchParam(type: OrmSearchParamType::In, field: 'id')]
    public ?array $ids = null;

    #[OrmSearchParam(type: OrmSearchParamType::Range, direction: OrmSearchParamDirection::From, field: 'createdAt')]
    public ?DateTimeImmutable $createdFrom = null;

    #[OrmSearchParam(type: OrmSearchParamType::Range, direction: OrmSearchParamDirection::To, field: 'createdAt')]
    public ?DateTimeImmutable $createdTo = null;

    #[OrmSearchParam(type: OrmSearchParamType::StringEnum)]
    public ?UserRole $role = null;  // backed enum, ->value used automatically

    // Properties with no #[OrmSearchParam] are silently ignored
    public ?bool $csv = null;
}
```

### 2. Parse in the repository

[](#2-parse-in-the-repository)

```
use Vidic\OrmSearchParam\OrmSearchParamParser;

public function search(UserSearchFilter $filter): array
{
    $alias = OrmSearchParamParser::getAlias($filter);
    $qb    = $this->createQueryBuilder($alias);
    OrmSearchParamParser::parse($filter, $qb);

    return $qb->getQuery()->getResult();
}
```

`parse()` only adds `andWhere` conditions for non-null properties. Null values are skipped entirely.

---

Parameter Types
---------------

[](#parameter-types)

TypeDQL producedNotes`Equals``alias.field = :param``Like``alias.field LIKE :param`wraps value in `%…%``NotLike``alias.field NOT LIKE :param`wraps value in `%…%``In``alias.field IN(:param)`value must be an array`NotIn``alias.field NOT IN(:param)`value must be an array`Range``alias.field >= :param` or `alias.field value` on a string-backed enum`IntEnum``alias.field = :param`uses `->value` on an int-backed enum`Callable`customcalls a static method you provide### Range direction

[](#range-direction)

```
#[OrmSearchParam(type: OrmSearchParamType::Range, direction: OrmSearchParamDirection::From, field: 'createdAt')]
public ?DateTimeImmutable $createdFrom = null;

#[OrmSearchParam(type: OrmSearchParamType::Range, direction: OrmSearchParamDirection::To, field: 'createdAt')]
public ?DateTimeImmutable $createdTo = null;
```

Two properties can target the same field — the parameter key is always the property name (`createdFrom`, `createdTo`), so there is no collision.

### Custom field name

[](#custom-field-name)

By default, the field name is the property name. Override with `field:`:

```
#[OrmSearchParam(type: OrmSearchParamType::Equals, field: 'email')]
public ?string $emailExact = null;
// generates: alias.email = :emailExact
```

---

Callable Type
-------------

[](#callable-type)

For complex conditions (joins, OR clauses, subqueries), use `Callable` with a static method:

```
#[OrmSearchParam(type: OrmSearchParamType::Callable, callable: [self::class, 'applyExcludeInternal'])]
public ?bool $excludeInternal = null;

public static function applyExcludeInternal(QueryBuilder $qb, bool $value, string $alias, object $filter): void
{
    if (!$value) {
        return;
    }
    OrmSearchParamParser::ensureJoin($qb, $alias, 'user', 'u');
    $qb->andWhere('u.email NOT LIKE :internalEmail')
       ->setParameter('internalEmail', '%@example.com');
}
```

The callable signature is always `(QueryBuilder $qb, mixed $value, string $alias, object $filter): void`.

### ensureJoin

[](#ensurejoin)

When multiple callables need the same join, use `ensureJoin` to avoid duplicate `LEFT JOIN` clauses:

```
OrmSearchParamParser::ensureJoin($qb, $alias, 'invoice', 'invoice');
```

If the join alias is already present on the QueryBuilder, the call is a no-op.

---

Parser API
----------

[](#parser-api)

```
// Read the #[OrmRootAlias] from a filter class
OrmSearchParamParser::getAlias(object $filter): string

// Apply all non-null #[OrmSearchParam] properties to the QueryBuilder
OrmSearchParamParser::parse(object $filter, QueryBuilder $qb): void

// Add a LEFT JOIN only if not already present
OrmSearchParamParser::ensureJoin(QueryBuilder $qb, string $alias, string $relation, string $joinAlias): void
```

---

Running Tests
-------------

[](#running-tests)

```
./docker.sh
```

Or locally (requires PHP 8.4 and `pdo_sqlite`):

```
composer install
php vendor/bin/phpunit tests
```

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance91

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity32

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% 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

Unknown

Total

1

Last Release

43d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/844d542d50bf5395bd6121ed513aff15bc38e001ed0c9bae67669b8f6950dc17?d=identicon)[viduli](/maintainers/viduli)

---

Top Contributors

[![vidic-igor](https://avatars.githubusercontent.com/u/56257335?v=4)](https://github.com/vidic-igor "vidic-igor (5 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/vidic-orm-search-param/health.svg)

```
[![Health](https://phpackages.com/badges/vidic-orm-search-param/health.svg)](https://phpackages.com/packages/vidic-orm-search-param)
```

###  Alternatives

[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

1155.2k](/packages/rcsofttech-audit-trail-bundle)[kimai/kimai

Kimai - Time Tracking

4.7k8.7k1](/packages/kimai-kimai)[ahmed-bhs/doctrine-doctor

Runtime analysis tool for Doctrine ORM integrated into Symfony Web Profiler. Unlike static linters, it analyzes actual query execution at runtime to detect performance bottlenecks, security vulnerabilities, and best practice violations during development with real execution context and data.

929.0k](/packages/ahmed-bhs-doctrine-doctor)

PHPackages © 2026

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