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(2mo ago)14MITPHPPHP ^8.1CI passing

Since Apr 27Pushed 2mo 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 3w 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

33

—

LowBetter than 72% of packages

Maintenance83

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

88d 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

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M400](/packages/easycorp-easyadmin-bundle)[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.

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

Kimai - Time Tracking

4.8k9.0k1](/packages/kimai-kimai)

PHPackages © 2026

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