PHPackages                             novabytes/laravel-odata - 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. novabytes/laravel-odata

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

novabytes/laravel-odata
=======================

Apply OData 4 query options to Eloquent models. Supports $filter, $select, $expand, $orderby, $top, $skip, and $count.

v0.1.1(yesterday)01MITPHPPHP ^8.2CI passing

Since Mar 20Pushed yesterdayCompare

[ Source](https://github.com/novabytes-labs/laravel-odata)[ Packagist](https://packagist.org/packages/novabytes/laravel-odata)[ RSS](/packages/novabytes-laravel-odata/feed)WikiDiscussions master Synced today

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

Laravel OData
=============

[](#laravel-odata)

[![Latest Version on Packagist](https://camo.githubusercontent.com/e0c45df7d931361d37269f1677a56562cde3c7ffb3b4efa320518ed5abae881a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e6f766162797465732f6c61726176656c2d6f646174612e737667)](https://packagist.org/packages/novabytes/laravel-odata)[![Test Status](https://camo.githubusercontent.com/5663d321af4844012ecdea54a7c6bc2b35dc81b37228ceaa57b77f5ff594d228/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e6f766162797465732d6c6162732f6c61726176656c2d6f646174612f63692e796d6c3f6c6162656c3d7465737473266272616e63683d6d6173746572)](https://camo.githubusercontent.com/5663d321af4844012ecdea54a7c6bc2b35dc81b37228ceaa57b77f5ff594d228/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e6f766162797465732d6c6162732f6c61726176656c2d6f646174612f63692e796d6c3f6c6162656c3d7465737473266272616e63683d6d6173746572)[![Code Style Status](https://camo.githubusercontent.com/9c6d72afbfa83bbd2d1bf7458c10c79c3a519bd8bfe1ff2c44452b69384fdf45/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e6f766162797465732d6c6162732f6c61726176656c2d6f646174612f63692e796d6c3f6c6162656c3d636f64652532307374796c65266272616e63683d6d6173746572)](https://camo.githubusercontent.com/9c6d72afbfa83bbd2d1bf7458c10c79c3a519bd8bfe1ff2c44452b69384fdf45/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e6f766162797465732d6c6162732f6c61726176656c2d6f646174612f63692e796d6c3f6c6162656c3d636f64652532307374796c65266272616e63683d6d6173746572)[![Total Downloads](https://camo.githubusercontent.com/3b05339fb3fd40ef84f477c3d8f3108d20668c960dd8235747919fa4a00caa41/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6e6f766162797465732f6c61726176656c2d6f646174612e737667)](https://packagist.org/packages/novabytes/laravel-odata)

Apply OData 4 query options to Eloquent models. Supports `$filter`, `$select`, `$expand`, `$orderby`, `$top`, `$skip`, and `$count`.

Built on top of [novabytes/odata-query-parser](https://github.com/novabytes-labs/odata-query-parser).

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

[](#installation)

```
composer require novabytes/laravel-odata
```

Publish the config file:

```
php artisan vendor:publish --tag=odata-config
```

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

[](#quick-start)

```
use NovaBytes\OData\Laravel\ODataQueryBuilder;

class ProductController extends Controller
{
    public function index(Request $request)
    {
        return ODataQueryBuilder::for(Product::class, $request)
            ->allowedFilters('name', 'price', 'is_active', 'category_id')
            ->allowedSorts('name', 'price', 'created_at')
            ->allowedExpands('category', 'reviews')
            ->allowedSelects('id', 'name', 'price', 'description', 'category_id')
            ->get();
    }
}
```

Your API now accepts OData queries:

```
GET /products?$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

```

Usage
-----

[](#usage)

### Filtering (`$filter`)

[](#filtering-filter)

```
GET /products?$filter=Price gt 100
GET /products?$filter=Name eq 'Milk'
GET /products?$filter=Price gt 5 and Price lt 20
GET /products?$filter=Name eq 'Milk' or Name eq 'Cheese'
GET /products?$filter=contains(Name,'Widget')
GET /products?$filter=startswith(Name,'Ch')
GET /products?$filter=IsActive eq true
GET /products?$filter=Name in ('Milk','Cheese','Butter')
GET /products?$filter=Description eq null
GET /products?$filter=Reviews/any(r:r/Rating gt 4)

```

All OData comparison operators (`eq`, `ne`, `gt`, `ge`, `lt`, `le`), logical operators (`and`, `or`, `not`), and 30+ built-in functions are supported by the parser. See the [parser README](https://github.com/novabytes-labs/odata-query-parser) for the full list.

### Selecting (`$select`)

[](#selecting-select)

```
GET /products?$select=Name,Price
GET /products?$select=*

```

The primary key is always included automatically to ensure relationships work correctly.

### Expanding (`$expand`)

[](#expanding-expand)

```
GET /products?$expand=Category
GET /products?$expand=Category,Reviews
GET /products?$expand=Reviews($filter=Rating gt 4;$top=5;$orderby=Rating desc)

```

Nested query options inside `$expand` are separated by `;` (per the OData spec) and support `$filter`, `$select`, `$orderby`, `$top`, and `$skip`.

### Sorting (`$orderby`)

[](#sorting-orderby)

```
GET /products?$orderby=Name asc
GET /products?$orderby=Price desc
GET /products?$orderby=IsActive desc,Price asc

```

### Pagination (`$top`, `$skip`)

[](#pagination-top-skip)

```
GET /products?$top=10
GET /products?$top=10&$skip=20

```

### Count (`$count`)

[](#count-count)

```
GET /products?$count=true&$top=10

```

When `$count=true`, the response includes total count metadata and returns paginated results.

PascalCase Conversion
---------------------

[](#pascalcase-conversion)

OData uses PascalCase property names (`Price`, `CategoryId`, `IsActive`). This package automatically converts them to Eloquent's snake\_case convention:

ODataEloquent`Price``price``CategoryId``category_id``IsActive``is_active``CreatedAt``created_at``Category` (expand)`category` (relationship)You define your allowlists in snake\_case — the conversion is handled for you.

Security
--------

[](#security)

### Allowlists

[](#allowlists)

Every filterable, sortable, expandable, and selectable field must be explicitly whitelisted. Any request for a field not in the allowlist throws a `400 Bad Request` by default.

```
ODataQueryBuilder::for(Product::class, $request)
    ->allowedFilters('name', 'price')       // Only these can be filtered
    ->allowedSorts('name', 'created_at')    // Only these can be sorted
    ->allowedExpands('category')            // Only these can be expanded
    ->allowedSelects('id', 'name', 'price') // Only these can be selected
    ->get();
```

If no allowlist is set for a given operation (e.g. `allowedFilters` is never called), that operation is unrestricted.

### Expand Depth Limit

[](#expand-depth-limit)

Prevents abuse via deeply nested `$expand`:

```
// config/odata.php
'max_expand_depth' => 3, // default
```

`$expand=A($expand=B($expand=C($expand=D)))` would be rejected at depth 4.

### Top Limit

[](#top-limit)

Prevents clients from requesting excessive result sets:

```
// config/odata.php
'max_top' => 1000, // default
```

Configuration
-------------

[](#configuration)

```
// config/odata.php

return [
    // 'laravel' or 'odata'
    'response_format' => 'laravel',

    // Maximum $expand nesting depth
    'max_expand_depth' => 3,

    // Maximum $top value (null = unlimited)
    'max_top' => 1000,

    // Default $top when client doesn't specify (null = no limit)
    'default_top' => null,

    // true = throw 400 on invalid operations, false = silently ignore
    'throw_on_invalid' => true,
];
```

Response Format
---------------

[](#response-format)

### Laravel format (default)

[](#laravel-format-default)

```
{
    "data": [
        {"id": 1, "name": "Laptop", "price": 999.99}
    ],
    "meta": {
        "total": 100,
        "per_page": 10,
        "current_page": 1,
        "last_page": 10
    }
}
```

The `meta` key is only included when `$count=true`.

### OData format

[](#odata-format)

Set `response_format` to `'odata'` in config:

```
{
    "@odata.count": 100,
    "value": [
        {"id": 1, "name": "Laptop", "price": 999.99}
    ],
    "@odata.nextLink": "/products?$skip=10&$top=10"
}
```

Advanced Usage
--------------

[](#advanced-usage)

### Using `toBuilder()`

[](#using-tobuilder)

Get the Eloquent builder back for further customization:

```
$builder = ODataQueryBuilder::for(Product::class, $request)
    ->allowedFilters('price')
    ->allowedSorts('name')
    ->toBuilder();

// Add your own conditions
$results = $builder
    ->where('is_active', true)
    ->withCount('reviews')
    ->get();
```

### Existing query as starting point

[](#existing-query-as-starting-point)

Pass an existing builder instead of a model class:

```
$query = Product::where('is_active', true);

$results = ODataQueryBuilder::for($query, $request)
    ->allowedFilters('name', 'price')
    ->get();
```

### Lambda expressions

[](#lambda-expressions)

OData lambda expressions (`any`/`all`) are translated to Eloquent's `whereHas`/`whereDoesntHave`:

```
GET /products?$filter=Reviews/any(r:r/Rating gt 4)

```

Translates to:

```
Product::whereHas('reviews', fn($q) => $q->where('rating', '>', 4))
```

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

[](#requirements)

- PHP &gt;= 8.2
- Laravel 11, 12, or 13

License
-------

[](#license)

MIT

###  Health Score

38

—

LowBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

 Bus Factor1

Top contributor holds 92.3% 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 ~0 days

Total

2

Last Release

1d 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 (12 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

apilaraveleloquentqueryfilterodata

###  Code Quality

TestsPHPUnit

Code StylePHP CS Fixer

### Embed Badge

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

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

###  Alternatives

[tucker-eric/eloquentfilter

An Eloquent way to filter Eloquent Models

1.8k4.7M23](/packages/tucker-eric-eloquentfilter)[mehdi-fathi/eloquent-filter

Eloquent Filter adds custom filters automatically to your Eloquent Models in Laravel.It's easy to use and fully dynamic, just with sending the Query Strings to it.

450188.2k1](/packages/mehdi-fathi-eloquent-filter)[jedrzej/searchable

Searchable trait for Laravel's Eloquent models - filter your models using request parameters

127256.0k4](/packages/jedrzej-searchable)[aldemeery/sieve

A simple, clean and elegant way to filter Eloquent models.

1396.3k](/packages/aldemeery-sieve)

PHPackages © 2026

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