PHPackages                             novabytes/odata-query-parser - 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. [HTTP &amp; Networking](/categories/http)
4. /
5. novabytes/odata-query-parser

ActiveLibrary[HTTP &amp; Networking](/categories/http)

novabytes/odata-query-parser
============================

Framework-agnostic OData 4 query string parser for PHP. Parses $filter, $select, $expand, $orderby, and more into AST objects.

v0.1.0(1mo ago)035—0%1MITPHPPHP ^8.2CI passing

Since Mar 20Pushed 1mo agoCompare

[ Source](https://github.com/novabytes-labs/odata-query-parser)[ Packagist](https://packagist.org/packages/novabytes/odata-query-parser)[ RSS](/packages/novabytes-odata-query-parser/feed)WikiDiscussions master Synced 1mo ago

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

OData Query Parser for PHP
==========================

[](#odata-query-parser-for-php)

[![Latest Version on Packagist](https://camo.githubusercontent.com/a944608189a332f34bee251999506798088e1a8264991b82ad68974b51ccdf80/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e6f766162797465732f6f646174612d71756572792d7061727365722e737667)](https://packagist.org/packages/novabytes/odata-query-parser)[![Test Status](https://camo.githubusercontent.com/2686d5e60f235858b364e626deeeda260df3514bf2dab0eb71f970874d93af7f/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e6f766162797465732d6c6162732f6f646174612d71756572792d7061727365722f63692e796d6c3f6c6162656c3d7465737473266272616e63683d6d6173746572)](https://camo.githubusercontent.com/2686d5e60f235858b364e626deeeda260df3514bf2dab0eb71f970874d93af7f/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e6f766162797465732d6c6162732f6f646174612d71756572792d7061727365722f63692e796d6c3f6c6162656c3d7465737473266272616e63683d6d6173746572)[![Code Style Status](https://camo.githubusercontent.com/1d05c5c1526b14b4943a19e8d8bb34f854692a22bccc24e69c605ad3aa241f01/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e6f766162797465732d6c6162732f6f646174612d71756572792d7061727365722f63692e796d6c3f6c6162656c3d636f64652532307374796c65266272616e63683d6d6173746572)](https://camo.githubusercontent.com/1d05c5c1526b14b4943a19e8d8bb34f854692a22bccc24e69c605ad3aa241f01/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e6f766162797465732d6c6162732f6f646174612d71756572792d7061727365722f63692e796d6c3f6c6162656c3d636f64652532307374796c65266272616e63683d6d6173746572)[![Total Downloads](https://camo.githubusercontent.com/125f821cec8ad7a66a8913da39b576adb271004efd50c40d5fb2a75dde3c3e8f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6e6f766162797465732f6f646174612d71756572792d7061727365722e737667)](https://packagist.org/packages/novabytes/odata-query-parser)

A framework-agnostic OData 4 query string parser for PHP 8.2+. Parses `$filter`, `$select`, `$expand`, `$orderby`, `$top`, `$skip`, and `$count` into immutable AST objects.

Zero runtime dependencies.

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

[](#installation)

```
composer require novabytes/odata-query-parser
```

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

[](#quick-start)

```
use NovaBytes\OData\Parser\QueryOptionParser;

$query = QueryOptionParser::parse(
    '$filter=Price gt 100 and contains(Name,\'Widget\')'
    . '&$select=Name,Price'
    . '&$expand=Category($select=Name;$top=5)'
    . '&$orderby=Price desc'
    . '&$top=50&$skip=10&$count=true'
);

$query->filter;   // BinaryExpression (and)
$query->select;   // [SelectItem('Name'), SelectItem('Price')]
$query->expand;   // [ExpandItem('Category', nestedOptions: ...)]
$query->orderby;  // [OrderByItem(PropertyPath('Price'), Desc)]
$query->top;      // 50
$query->skip;     // 10
$query->count;    // true
```

You can also parse individual query options directly:

```
use NovaBytes\OData\Parser\FilterParser;
use NovaBytes\OData\Parser\SelectParser;
use NovaBytes\OData\Parser\OrderByParser;

$filter = FilterParser::parse('Price gt 100 and contains(Name,\'Widget\')');
$select = SelectParser::parse('Name,Price,Address/City');
$orderby = OrderByParser::parse('Name asc,Price desc');
```

Supported Query Options
-----------------------

[](#supported-query-options)

### `$filter`

[](#filter)

Full expression language with correct operator precedence:

```
// Comparison operators: eq, ne, gt, ge, lt, le
FilterParser::parse('Price gt 100');
FilterParser::parse('Name eq \'Milk\'');

// Logical operators: and, or, not
FilterParser::parse('Price gt 5 and Price lt 20');
FilterParser::parse('not contains(Name,\'test\')');

// Arithmetic operators: add, sub, mul, div, divby, mod
FilterParser::parse('Price mul Quantity gt 1000');

// Grouping with parentheses
FilterParser::parse('(Name eq \'A\' or Name eq \'B\') and Price lt 10');

// Property paths
FilterParser::parse('Address/City eq \'London\'');

// The in operator
FilterParser::parse('Name in (\'Milk\',\'Cheese\',\'Butter\')');
```

**30+ built-in functions:**

```
// String functions
FilterParser::parse('contains(Name,\'milk\')');
FilterParser::parse('startswith(Name,\'Ch\')');
FilterParser::parse('endswith(Name,\'ilk\')');
FilterParser::parse('length(Name) gt 5');
FilterParser::parse('indexof(Name,\'lk\') eq 2');
FilterParser::parse('substring(Name,1,3) eq \'ilk\'');
FilterParser::parse('tolower(Name) eq \'milk\'');
FilterParser::parse('toupper(Name) eq \'MILK\'');
FilterParser::parse('trim(Name) eq \'Milk\'');
FilterParser::parse('concat(FirstName,LastName) eq \'JohnDoe\'');

// Date/time functions
FilterParser::parse('year(BirthDate) eq 1990');
FilterParser::parse('month(BirthDate) eq 3');
FilterParser::parse('day(BirthDate) eq 20');
FilterParser::parse('hour(StartTime) ge 9');
FilterParser::parse('Date gt now()');

// Math functions
FilterParser::parse('round(Price) eq 10');
FilterParser::parse('floor(Price) eq 9');
FilterParser::parse('ceiling(Price) eq 10');
```

**Lambda expressions:**

```
// any — true if any element matches
FilterParser::parse('Items/any(d:d/Qty gt 100)');

// any without predicate — true if collection is non-empty
FilterParser::parse('Tags/any()');

// all — true if all elements match
FilterParser::parse('Items/all(d:d/Price gt 0)');
```

**All literal types:**

```
FilterParser::parse('Active eq true');                                    // boolean
FilterParser::parse('Name eq null');                                      // null
FilterParser::parse('Count eq 42');                                       // integer
FilterParser::parse('Price lt 9.99');                                     // decimal
FilterParser::parse('Name eq \'O\'\'Brien\'');                            // string (escaped quotes)
FilterParser::parse('Id eq 01234567-89ab-cdef-0123-456789abcdef');        // GUID
FilterParser::parse('BirthDate eq 2023-01-15');                           // date
FilterParser::parse('Created eq 2023-01-15T14:30:00Z');                   // DateTimeOffset
FilterParser::parse('Duration eq duration\'P1DT2H30M\'');                 // duration
```

### `$select`

[](#select)

```
$items = SelectParser::parse('Name,Price,Address/City');
// [SelectItem(['Name']), SelectItem(['Price']), SelectItem(['Address', 'City'])]

$items = SelectParser::parse('*');
// [SelectItem([], isWildcard: true)]
```

### `$expand`

[](#expand)

```
use NovaBytes\OData\Parser\ExpandParser;

$items = ExpandParser::parse('Products,Category');
// [ExpandItem(['Products']), ExpandItem(['Category'])]

// With nested query options (semicolon-separated inside parentheses)
$items = ExpandParser::parse('Products($filter=Price gt 100;$select=Name;$top=5)');
// ExpandItem(['Products'], nestedOptions: QueryOptions(filter: ..., select: ..., top: 5))
```

### `$orderby`

[](#orderby)

```
$items = OrderByParser::parse('Name asc,Price desc');
// [OrderByItem(PropertyPath('Name'), Asc), OrderByItem(PropertyPath('Price'), Desc)]

// Default direction is ascending
$items = OrderByParser::parse('Name');
// [OrderByItem(PropertyPath('Name'), Asc)]
```

### `$top`, `$skip`, `$count`

[](#top-skip-count)

Parsed as part of `QueryOptionParser::parse()`:

```
$query = QueryOptionParser::parse('$top=10&$skip=20&$count=true');
$query->top;   // 10
$query->skip;  // 20
$query->count; // true
```

AST Structure
-------------

[](#ast-structure)

Every parsed result is an immutable (`readonly class`) AST node. The `$filter` expression tree uses these node types:

NodeDescription`BinaryExpression``left operator right` (e.g. `Price gt 100`, `A and B`)`UnaryExpression``operator operand` (e.g. `not expr`, `-5`)`PropertyPath`Dotted/slashed property reference (e.g. `Address/City`)`Literal`Typed value: null, boolean, integer, decimal, string, GUID, date, etc.`FunctionCall`Built-in function with arguments (e.g. `contains(Name,'x')`)`LambdaExpression``collection/any(var:predicate)` or `collection/all(var:predicate)``ListExpression`Parenthesized list for `in` operator (e.g. `('A','B','C')`)Visitor Pattern
---------------

[](#visitor-pattern)

Implement `ExpressionVisitor` to transform the AST into whatever you need:

```
use NovaBytes\OData\AST\Filter\BinaryExpression;
use NovaBytes\OData\AST\Filter\Literal;
use NovaBytes\OData\AST\Filter\PropertyPath;
use NovaBytes\OData\Visitor\ExpressionVisitor;

class SqlWhereVisitor implements ExpressionVisitor
{
    public function visitBinaryExpression(BinaryExpression $expr): string
    {
        $left = $this->visit($expr->left);
        $right = $this->visit($expr->right);
        $op = match($expr->operator) {
            BinaryOperator::Eq => '=',
            BinaryOperator::Ne => '!=',
            BinaryOperator::Gt => '>',
            // ...
        };
        return "{$left} {$op} {$right}";
    }

    public function visitPropertyPath(PropertyPath $expr): string
    {
        return implode('.', $expr->segments);
    }

    public function visitLiteral(Literal $expr): string
    {
        // Use parameterized queries in real code!
        return match($expr->type) {
            LiteralType::String => "'{$expr->value}'",
            LiteralType::Null => 'NULL',
            default => (string) $expr->value,
        };
    }

    // ... implement remaining visit methods
}
```

A `StringifyVisitor` is included for round-tripping AST back to OData syntax:

```
use NovaBytes\OData\Visitor\StringifyVisitor;

$expr = FilterParser::parse('Price gt 100 and Name eq \'Milk\'');
$visitor = new StringifyVisitor();
echo $visitor->stringify($expr);
// Price gt 100 and Name eq 'Milk'
```

Error Handling
--------------

[](#error-handling)

All parse errors throw `NovaBytes\OData\Exception\ParseException` with position information:

```
try {
    FilterParser::parse('Price gtt 100');
} catch (ParseException $e) {
    $e->getMessage();  // "Unexpected 'gtt' at position 6; expected ..."
    $e->position;      // 6
}
```

OData 4 Support
---------------

[](#odata-4-support)

### System Query Options

[](#system-query-options)

Query OptionStatusNotes`$filter`SupportedFull expression language with correct operator precedence`$select`SupportedProperty paths, wildcards (`*`), nested options`$expand`SupportedNavigation paths, nested query options (`$filter`, `$select`, `$top`, etc.)`$orderby`SupportedExpressions with `asc`/`desc`, multiple sort keys`$top`Supported`$skip`Supported`$count`SupportedInline count (`true`/`false`)`$search`Not yetPlanned for a future release`$compute`Not yetPlanned for a future release`$apply`Not yetData aggregation extension, planned for a future release`$format`Not yet`$index`Not yet`$schemaversion`Not yet`$skiptoken`Not yetOpaque server-driven paging token`$deltatoken`Not yetOpaque server-driven delta token### Filter Operators

[](#filter-operators)

CategoryOperatorsStatusComparison`eq`, `ne`, `gt`, `ge`, `lt`, `le`SupportedLogical`and`, `or`, `not`SupportedArithmetic`add`, `sub`, `mul`, `div`, `divby`, `mod`SupportedMembership`in`SupportedEnum flags`has`SupportedGrouping`( )`SupportedLambda`any`, `all`SupportedNegation`-` (unary minus)Supported### Filter Functions

[](#filter-functions)

CategoryFunctionsStatusString`contains`, `startswith`, `endswith`, `length`, `indexof`, `substring`, `tolower`, `toupper`, `trim`, `concat`, `matchesPattern`SupportedDate/Time`year`, `month`, `day`, `hour`, `minute`, `second`, `fractionalseconds`, `totalseconds`, `date`, `time`, `totaloffsetminutes`, `now`, `mindatetime`, `maxdatetime`SupportedMath`round`, `floor`, `ceiling`SupportedGeo`geo.distance`, `geo.length`, `geo.intersects`SupportedCollection`hassubset`, `hassubsequence`SupportedType`cast`, `isof`Supported### Literal Types

[](#literal-types)

TypeExampleStatusNull`null`SupportedBoolean`true`, `false`SupportedInteger`42`, `-1`SupportedDecimal`3.14`, `1.5e10`SupportedString`'Milk'`, `'O''Brien'`SupportedGUID`01234567-89ab-cdef-0123-456789abcdef`SupportedDate`2023-01-15`SupportedDateTimeOffset`2023-01-15T14:30:00Z`SupportedTimeOfDay`14:30:00`SupportedDuration`duration'P1DT2H30M'`SupportedNaN / Infinity`NaN`, `INF`, `-INF`SupportedBinary`binary'T0RhdGE='`Not yetEnum`Namespace.Color'Red'`Not yetGeography/Geometry`geography'SRID=0;Point(...)'`Not yetDesign Decisions
----------------

[](#design-decisions)

- **Framework-agnostic** -- zero dependencies, works with any PHP 8.2+ project. Use it with Laravel, Symfony, API Platform, Slim, or plain PHP.
- **Schema-unaware** -- the parser does not validate property names or types against a data model. It parses syntax only. Schema validation belongs in a separate layer.
- **Immutable AST** -- all nodes are `readonly class`, safe to cache and share.
- **Pratt parser** -- the `$filter` expression parser uses top-down operator precedence parsing for correct handling of all 11 precedence levels defined in the OData 4.01 spec.

Requirements
------------

[](#requirements)

- PHP &gt;= 8.2

License
-------

[](#license)

MIT

###  Health Score

38

—

LowBetter than 85% of packages

Maintenance90

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity36

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

51d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/0c2ab9193e49e602acf43f6a345525a10675c2957cc8990fc1ac0978900307e6?d=identicon)[NovaBytes](/maintainers/NovaBytes)

---

Top Contributors

[![Eligioo](https://avatars.githubusercontent.com/u/8634939?v=4)](https://github.com/Eligioo "Eligioo (9 commits)")

---

Tags

apirestparserqueryfilterastodata

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/novabytes-odata-query-parser/health.svg)

```
[![Health](https://phpackages.com/badges/novabytes-odata-query-parser/health.svg)](https://phpackages.com/packages/novabytes-odata-query-parser)
```

###  Alternatives

[graviton/rql-parser

3196.2k3](/packages/graviton-rql-parser)[illuminatech/data-provider

Allows easy build for DB queries from API requests

4413.3k](/packages/illuminatech-data-provider)[graviton/php-rql-parser

doctrine-odm query building wrapper to graviton/rql-parser

1758.6k1](/packages/graviton-php-rql-parser)[adrotec/breeze.server.php

Breeze JS support for Symfony 2 or any other PHP 5.3+ application

2615.8k1](/packages/adrotec-breezeserverphp)[bjerke/api-query-builder

A query builder for Laravel that parses the request and uses Eloquent ORM to query database

267.7k1](/packages/bjerke-api-query-builder)[bjerke/laravel-bread

A boilerplate package for BREAD operations through REST API in Laravel

115.2k](/packages/bjerke-laravel-bread)

PHPackages © 2026

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