{"id":50722013,"url":"https://github.com/imarc/fort","last_synced_at":"2026-06-10T01:01:34.793Z","repository":{"id":351170511,"uuid":"1209832584","full_name":"imarc/fort","owner":"imarc","description":"Safe API filtering \u0026 sorting for Laravel","archived":false,"fork":false,"pushed_at":"2026-04-13T21:53:12.000Z","size":31,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-13T23:21:46.120Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/imarc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-13T20:33:24.000Z","updated_at":"2026-04-13T21:52:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/imarc/fort","commit_stats":null,"previous_names":["imarc/fort"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/imarc/fort","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imarc%2Ffort","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imarc%2Ffort/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imarc%2Ffort/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imarc%2Ffort/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/imarc","download_url":"https://codeload.github.com/imarc/fort/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imarc%2Ffort/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34132030,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-10T01:01:33.563Z","updated_at":"2026-06-10T01:01:34.755Z","avatar_url":"https://github.com/imarc.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fort\n\n**Fort provides a safe, declarative layer for applying API filters and sorting to Eloquent queries.**\n\nInstead 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.\n\n## Features\n\n* Whitelist-driven filtering and sorting (`Filter::map` / `Sort::map`)\n* **`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\n* Default sort when the client omits `sort` (`defaultSorts()` on the request, or an argument to `sorts()`)\n* 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\n* **`HasFilterableQuery`** — models get a **`FilterableBuilder`** with `applyFilters()` / `applySorts()`\n* Multiple sort fields with direction (`-` for descending, optional `+` for ascending)\n\n---\n\n## Why Fort?\n\nMost Laravel apps start with request-driven query logic directly in controllers:\n\n```php\n$query-\u003ewhen($request-\u003einput('region_id'), fn ($q, $id) =\u003e\n    $q-\u003ewhere('region_id', $id)\n);\n```\n\nThis works, but it often leads to:\n\n* duplicated logic across controllers\n* tight coupling between request structure and query construction\n* inconsistent filtering and sorting behavior between endpoints\n* risk of accidentally exposing query behavior you did not intend to support\n\nFort takes a different approach. Type-hint **`FilterableRequest`** (or a subclass), build a whitelist map, and apply it in one place:\n\n```php\nProject::query()\n    -\u003eapplyFilters($request-\u003efilters(), $filters)\n    -\u003eapplySorts($request-\u003esorts(['-created']), $sorts);\n```\n\nWith Fort:\n\n* only explicitly allowed filters and sorts are applied\n* validation and parsing for `filters` / `sort` stay on the request; maps can live in the controller or on a subclass\n* custom behavior uses **`Filter`** / **`Sort`** helpers instead of ad hoc controller branches\n\n---\n\n## How It Works\n\n```text\nHTTP (filters[], sort[]) -\u003e FilterableRequest (validate + parse)\n                        -\u003e your filter/sort maps\n                        -\u003e applyFilters() / applySorts() on the Eloquent builder\n```\n\n1. **`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()`**.\n2. You pass whitelist definitions into **`applyFilters()`** / **`applySorts()`** as arrays, or return them from **`filterMap()`** / **`sortMap()`** when you extend the request (see below).\n3. Models using **`HasFilterableQuery`** (or a custom builder extending **`FilterableBuilder`**) get **`applyFilters()`** and **`applySorts()`** on the query builder.\n\n---\n\n## Installation\n\n```bash\ncomposer require imarc/fort\n```\n\n---\n\n## Recommended usage\n\n### Type-hint `FilterableRequest`\n\nThe 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.\n\n```php\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Imarc\\Fort\\Filters\\Filter;\nuse Imarc\\Fort\\Http\\Requests\\FilterableRequest;\nuse Imarc\\Fort\\Sorts\\Sort;\n\npublic function index(FilterableRequest $request)\n{\n    $filters = [\n        'region' =\u003e 'project.region_id',\n        'status' =\u003e Filter::callback(fn (Builder $q, mixed $v) =\u003e $q-\u003ewhere('status', $v)),\n    ];\n\n    $sorts = [\n        'name' =\u003e Sort::column('name'),\n        'created' =\u003e Sort::callback(fn (Builder $q, string $dir) =\u003e $q-\u003eorderBy('created_at', $dir)),\n    ];\n\n    $projects = Project::query()\n        -\u003eapplyFilters($request-\u003efilters(), $filters)\n        -\u003eapplySorts($request-\u003esorts(['-created']), $sorts)\n        -\u003eget();\n\n    return ProjectResource::collection($projects);\n}\n```\n\n**Default sort:** pass segments into **`sorts()`** when the query string has no `sort` (e.g. **`$request-\u003esorts(['-created_at'])`**). Pass **`sorts([])`** to apply no ordering when the query omits `sort`.\n\n---\n\n### Extend `FilterableRequest` for heavier endpoints\n\nWhen 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-\u003esorts()`** with no arguments applies a fallback when the client omits `sort`.\n\n```php\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Imarc\\Fort\\Filters\\Filter;\nuse Imarc\\Fort\\Http\\Requests\\FilterableRequest;\nuse Imarc\\Fort\\Sorts\\Sort;\n\nclass IndexProjectsRequest extends FilterableRequest\n{\n    public function filterMap(): array\n    {\n        return [\n            'region' =\u003e 'project.region_id',\n            'status' =\u003e Filter::callback(function (Builder $query, mixed $value): void {\n                $query-\u003ewhere('status', $value);\n            }),\n        ];\n    }\n\n    public function sortMap(): array\n    {\n        return [\n            'name' =\u003e Sort::column('name'),\n            'created' =\u003e Sort::callback(function (Builder $query, string $direction): void {\n                $query-\u003eorderBy('created_at', $direction);\n            }),\n        ];\n    }\n\n    protected function defaultSorts(): array\n    {\n        return ['-created'];\n    }\n}\n```\n\n```php\npublic function index(IndexProjectsRequest $request)\n{\n    $projects = Project::query()\n        -\u003eapplyFilters($request-\u003efilters(), $request-\u003efilterMap())\n        -\u003eapplySorts($request-\u003esorts(), $request-\u003esortMap())\n        -\u003eget();\n\n    return ProjectResource::collection($projects);\n}\n```\n\n---\n\n## Eloquent builder\n\nUse **`Imarc\\Fort\\Eloquent\\Concerns\\HasFilterableQuery`** on your model so **`Model::query()`** returns **`FilterableBuilder`**, which includes **`applyFilters()`** and **`applySorts()`**.\n\nIf you need extra builder methods, extend **`FilterableBuilder`** and override **`Model::newEloquentBuilder()`** (see the trait docblock).\n\n---\n\n## Request format\n\n### Filtering\n\n```http\nGET /projects?filters[region]=1\u0026filters[status]=active\n```\n\nOnly keys present in your filter map are applied. Nested values (e.g. date ranges) use array-style query parameters as usual for Laravel.\n\n---\n\n### Sorting\n\nA single value is accepted and normalized to an array internally:\n\n```http\nGET /projects?sort=name\nGET /projects?sort=-created\n```\n\nMultiple fields:\n\n```http\nGET /projects?sort[]=name\u0026sort[]=-created\n```\n\n* `-` prefix → descending  \n* `+` prefix → ascending (optional; default is ascending)\n\nSort keys are validated to a safe pattern (`[a-zA-Z0-9][a-zA-Z0-9_.]*`).\n\n---\n\n## Filter definitions\n\nMaps are **`array\u003cint|string, string|FilterDefinition\u003e`**. Fort normalizes them with **`Filter::map()`**:\n\n| Form | Result |\n|------|--------|\n| `'column'` or `key =\u003e 'column'` | **`ExactFilter`** on that column |\n| `'relation.nested.column'` | **`RelationExactFilter`** (`whereHas` style) |\n| `Filter::relationExact('relation', 'column')` | Explicit relation filter |\n| `Filter::callback(closure)` | Custom `(Builder $query, mixed $value, array $context)` |\n| `Filter::dateRange('column')` | Inclusive range; value array with optional `start` / `end` (`Y-m-d`) |\n| `Filter::builder(CustomBuilder::class, 'methodName')` | Delegates to a method on your builder when the query matches that class |\n\n**`FilterDefinition`** is the abstract base class; instantiate the concrete types above (or your own subclasses), not `FilterDefinition` directly.\n\n---\n\n## Sort definitions\n\nMaps are **`array\u003cint|string, string|SortDefinition\u003e`**, normalized with **`Sort::map()`**:\n\n| Form | Result |\n|------|--------|\n| `'column'` or `key =\u003e 'column'` | **`ColumnSort`** |\n| `Sort::column('column')` | Same, explicit |\n| `Sort::callback(closure)` | Custom `(Builder $query, string $direction, array $context)` |\n\n**`SortDefinition`** is abstract; use **`Sort::column()`** / **`Sort::callback()`** or your own subclasses.\n\n---\n\n## `applySorts()` signature\n\n**`applySorts()`** takes only the parsed sort list and the map — there is no third “default” argument. Defaults belong on the request: **`defaultSorts()`** or **`$request-\u003esorts([...])`**.\n\n---\n\n## Whitelisting\n\nFort ignores filters and sorts that are not in the map. That limits arbitrary query manipulation and keeps public APIs predictable.\n\n---\n\n## Philosophy\n\nFort is built around one core idea:\n\n**Nothing should affect your query unless you explicitly allow it.**\n\nThis 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.\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fimarc%2Ffort","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fimarc%2Ffort","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fimarc%2Ffort/lists"}