PHPackages                             imarc/fort - 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. [API Development](/categories/api)
4. /
5. imarc/fort

ActiveLibrary[API Development](/categories/api)

imarc/fort
==========

Safe API filtering &amp; sorting for Laravel

v1.0.4(1mo ago)035↓100%1MITPHPPHP ^8.2

Since Apr 13Pushed 1mo agoCompare

[ Source](https://github.com/imarc/fort)[ Packagist](https://packagist.org/packages/imarc/fort)[ RSS](/packages/imarc-fort/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (3)Versions (8)Used By (1)

Fort
====

[](#fort)

**Fort provides a safe, declarative layer for applying API filters and sorting to Eloquent queries.**

Instead of turning request parameters directly into database queries, Fort uses explicit mappings to control exactly what can be filtered and sorted, so your API stays predictable, secure, and maintainable.

Features
--------

[](#features)

- Whitelist-driven filtering and sorting (`Filter::map` / `Sort::map`)
- **`FilterableRequest`** — Laravel `FormRequest` that validates `filters` and `sort` and exposes **`filters()`** / **`sorts()`**; optionally override **`filterMap()`** / **`sortMap()`** / **`defaultSorts()`** in a subclass when you want maps and defaults on the request
- Default sort when the client omits `sort` (`defaultSorts()` on the request, or an argument to `sorts()`)
- String shorthands for column filters and sorts, plus **`Filter::callback`**, **`Sort::callback`**, **`Filter::dateRange`**, relation paths (`relation.column`), and **`Filter::builder`** for custom builder methods
- **`HasFilterableQuery`** — models get a **`FilterableBuilder`** with `applyFilters()` / `applySorts()`
- Multiple sort fields with direction (`-` for descending, optional `+` for ascending)

---

Why Fort?
---------

[](#why-fort)

Most Laravel apps start with request-driven query logic directly in controllers:

```
$query->when($request->input('region_id'), fn ($q, $id) =>
    $q->where('region_id', $id)
);
```

This works, but it often leads to:

- duplicated logic across controllers
- tight coupling between request structure and query construction
- inconsistent filtering and sorting behavior between endpoints
- risk of accidentally exposing query behavior you did not intend to support

Fort takes a different approach. Type-hint **`FilterableRequest`** (or a subclass), build a whitelist map, and apply it in one place:

```
Project::query()
    ->applyFilters($request->filters(), $filters)
    ->applySorts($request->sorts(['-created']), $sorts);
```

With Fort:

- only explicitly allowed filters and sorts are applied
- validation and parsing for `filters` / `sort` stay on the request; maps can live in the controller or on a subclass
- custom behavior uses **`Filter`** / **`Sort`** helpers instead of ad hoc controller branches

---

How It Works
------------

[](#how-it-works)

```
HTTP (filters[], sort[]) -> FilterableRequest (validate + parse)
                        -> your filter/sort maps
                        -> applyFilters() / applySorts() on the Eloquent builder

```

1. **`FilterableRequest`** validates structure for `filters` and `sort`, normalizes a single `sort=foo` query value into an array, and parses sort segments into `{ key, direction }` entries for **`applySorts()`**.
2. You pass whitelist definitions into **`applyFilters()`** / **`applySorts()`** as arrays, or return them from **`filterMap()`** / **`sortMap()`** when you extend the request (see below).
3. Models using **`HasFilterableQuery`** (or a custom builder extending **`FilterableBuilder`**) get **`applyFilters()`** and **`applySorts()`** on the query builder.

---

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

[](#installation)

```
composer require imarc/fort
```

---

Recommended usage
-----------------

[](#recommended-usage)

### Type-hint `FilterableRequest`

[](#type-hint-filterablerequest)

The usual starting point is to type-hint the base request class and define filter and sort maps next to the query (commonly in the controller). **`FilterableRequest`** only validates and exposes **`filters()`** and **`sorts()`**; it does not require a subclass.

```
use Illuminate\Database\Eloquent\Builder;
use Imarc\Fort\Filters\Filter;
use Imarc\Fort\Http\Requests\FilterableRequest;
use Imarc\Fort\Sorts\Sort;

public function index(FilterableRequest $request)
{
    $filters = [
        'region' => 'project.region_id',
        'status' => Filter::callback(fn (Builder $q, mixed $v) => $q->where('status', $v)),
    ];

    $sorts = [
        'name' => Sort::column('name'),
        'created' => Sort::callback(fn (Builder $q, string $dir) => $q->orderBy('created_at', $dir)),
    ];

    $projects = Project::query()
        ->applyFilters($request->filters(), $filters)
        ->applySorts($request->sorts(['-created']), $sorts)
        ->get();

    return ProjectResource::collection($projects);
}
```

**Default sort:** pass segments into **`sorts()`** when the query string has no `sort` (e.g. **`$request->sorts(['-created_at'])`**). Pass **`sorts([])`** to apply no ordering when the query omits `sort`.

---

### Extend `FilterableRequest` for heavier endpoints

[](#extend-filterablerequest-for-heavier-endpoints)

When maps grow large, you want defaults without repeating them at every call site, or you prefer colocating whitelist definitions with the same form request, subclass **`FilterableRequest`** and override **`filterMap()`** and **`sortMap()`**. Override **`defaultSorts()`** so **`$request->sorts()`** with no arguments applies a fallback when the client omits `sort`.

```
use Illuminate\Database\Eloquent\Builder;
use Imarc\Fort\Filters\Filter;
use Imarc\Fort\Http\Requests\FilterableRequest;
use Imarc\Fort\Sorts\Sort;

class IndexProjectsRequest extends FilterableRequest
{
    public function filterMap(): array
    {
        return [
            'region' => 'project.region_id',
            'status' => Filter::callback(function (Builder $query, mixed $value): void {
                $query->where('status', $value);
            }),
        ];
    }

    public function sortMap(): array
    {
        return [
            'name' => Sort::column('name'),
            'created' => Sort::callback(function (Builder $query, string $direction): void {
                $query->orderBy('created_at', $direction);
            }),
        ];
    }

    protected function defaultSorts(): array
    {
        return ['-created'];
    }
}
```

```
public function index(IndexProjectsRequest $request)
{
    $projects = Project::query()
        ->applyFilters($request->filters(), $request->filterMap())
        ->applySorts($request->sorts(), $request->sortMap())
        ->get();

    return ProjectResource::collection($projects);
}
```

---

Eloquent builder
----------------

[](#eloquent-builder)

Use **`Imarc\Fort\Eloquent\Concerns\HasFilterableQuery`** on your model so **`Model::query()`** returns **`FilterableBuilder`**, which includes **`applyFilters()`** and **`applySorts()`**.

If you need extra builder methods, extend **`FilterableBuilder`** and override **`Model::newEloquentBuilder()`** (see the trait docblock).

---

Request format
--------------

[](#request-format)

### Filtering

[](#filtering)

```
GET /projects?filters[region]=1&filters[status]=active
```

Only keys present in your filter map are applied. Nested values (e.g. date ranges) use array-style query parameters as usual for Laravel.

---

### Sorting

[](#sorting)

A single value is accepted and normalized to an array internally:

```
GET /projects?sort=name
GET /projects?sort=-created
```

Multiple fields:

```
GET /projects?sort[]=name&sort[]=-created
```

- `-` prefix → descending
- `+` prefix → ascending (optional; default is ascending)

Sort keys are validated to a safe pattern (`[a-zA-Z0-9][a-zA-Z0-9_.]*`).

---

Filter definitions
------------------

[](#filter-definitions)

Maps are **`array`**. Fort normalizes them with **`Filter::map()`**:

FormResult`'column'` or `key => 'column'`**`ExactFilter`** on that column`'relation.nested.column'`**`RelationExactFilter`** (`whereHas` style)`Filter::relationExact('relation', 'column')`Explicit relation filter`Filter::callback(closure)`Custom `(Builder $query, mixed $value, array $context)``Filter::dateRange('column')`Inclusive range; value array with optional `start` / `end` (`Y-m-d`)`Filter::builder(CustomBuilder::class, 'methodName')`Delegates to a method on your builder when the query matches that class**`FilterDefinition`** is the abstract base class; instantiate the concrete types above (or your own subclasses), not `FilterDefinition` directly.

---

Sort definitions
----------------

[](#sort-definitions)

Maps are **`array`**, normalized with **`Sort::map()`**:

FormResult`'column'` or `key => 'column'`**`ColumnSort`**`Sort::column('column')`Same, explicit`Sort::callback(closure)`Custom `(Builder $query, string $direction, array $context)`**`SortDefinition`** is abstract; use **`Sort::column()`** / **`Sort::callback()`** or your own subclasses.

---

`applySorts()` signature
------------------------

[](#applysorts-signature)

**`applySorts()`** takes only the parsed sort list and the map — there is no third “default” argument. Defaults belong on the request: **`defaultSorts()`** or **`$request->sorts([...])`**.

---

Whitelisting
------------

[](#whitelisting)

Fort ignores filters and sorts that are not in the map. That limits arbitrary query manipulation and keeps public APIs predictable.

---

Philosophy
----------

[](#philosophy)

Fort is built around one core idea:

**Nothing should affect your query unless you explicitly allow it.**

This makes it useful for public APIs, complex filtering, and teams that want consistent, reviewable rules — starting with a type-hinted **`FilterableRequest`**, and escalating to a subclass when maps and defaults deserve a dedicated form request.

---

License
-------

[](#license)

MIT

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance89

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 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

Every ~0 days

Total

7

Last Release

56d ago

Major Versions

v0.0.2 → v1.0.02026-04-13

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/42547992?v=4)[Dave Anastasi](/maintainers/daveanastasi)[@daveanastasi](https://github.com/daveanastasi)

---

Top Contributors

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

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/imarc-fort/health.svg)

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

###  Alternatives

[statamic/cms

The Statamic CMS Core Package

4.8k3.5M901](/packages/statamic-cms)[darkaonline/l5-swagger

OpenApi or Swagger integration to Laravel

2.9k36.4M124](/packages/darkaonline-l5-swagger)[knuckleswtf/scribe

Generate API documentation for humans from your Laravel codebase.✍

2.3k13.5M59](/packages/knuckleswtf-scribe)[mozex/anthropic-laravel

Laravel integration for the Anthropic API: facade, config publishing, install command, testing fakes, messages, streaming, tool use, thinking, and batches.

74287.1k1](/packages/mozex-anthropic-laravel)[justbetter/laravel-magento-client

A client to interact with Magento

49108.7k14](/packages/justbetter-laravel-magento-client)[scriptdevelop/whatsapp-manager

Paquete para manejo de WhatsApp Business API en Laravel

783.5k](/packages/scriptdevelop-whatsapp-manager)

PHPackages © 2026

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