You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: doc/Developing/Creating-API-Resources.md
+162-5Lines changed: 162 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -66,8 +66,11 @@ class LocationRepository extends Repository
66
66
67
67
-**`$model`** The Eloquent model class this repository wraps.
68
68
-**`$id`** Override if the model uses a non-standard primary key (e.g. `device_id`, `port_id`).
69
-
-**`fields()`** Defines which attributes are exposed in the API response. Use `->readonly()` for fields that should not be writable.
70
-
-**`$search`** Array of columns that the `?search=` parameter queries against.
69
+
-**`uriKey`** The URL slug for this resource (e.g. `devices`). Auto-derived from the class name; override to control the public name. See [Resource name & URLs](#resource-name--urls-urikey).
70
+
-**`$title`** The column used as the human-readable label for a record (shown in relationship pickers and some UIs).
71
+
-**`fields()`** Defines which attributes are exposed in the API response. Use `->readonly()` for fields that should not be writable. See [Fields and `->label()`](#fields-and-the-label-method).
72
+
-**`$search` / `searchables()`** Columns the `?search=` parameter queries against. See [Filtering, searching, sorting](#filtering-searching-and-sorting).
73
+
-**`related()`** Declares relationships (nested routes + `?include=`). See [Relationships](#relationships-related).
71
74
-**`indexQuery()` / `showQuery()`** Scope queries for access control. See existing `DeviceRepository` and `PortRepository` for examples using `$query->hasAccess($user)`.
`field('name')` exposes an attribute. The first argument is the **public JSON key** in the response. When it matches the column name, the value is read/written directly:
88
+
89
+
```php
90
+
field('hostname')->rules('required', 'string', 'max:255'), // reads/writes the hostname column
91
+
```
92
+
93
+
When the API name should differ from the database column, pass a **resolve callback** as the second argument (this is the convention used throughout LibreNMS to present clean names over legacy columns):
|`->readonly()`| Field is returned but never accepted on create/update. |
105
+
|`->rules('nullable', 'string', ...)`| Laravel validation applied on store/update. |
106
+
|`->hidden()`| Field is accepted for writes but omitted from responses. |
107
+
|`->label('name')`| Overrides the field's "attribute" used internally as `getAttribute() = label ?? attribute`. Rarely needed on plain fields; its important role is on **relationships**, where it sets the relationship's URL segment. |
108
+
109
+
> On **relationships** (`HasMany`/`BelongsTo`/`BelongsToMany`) the `label` is what controls the nested-route URL segment but you normally don't set it by hand: the base repository derives it automatically. See [Relationships](#relationships-related).
110
+
111
+
## Filtering, searching, and sorting
112
+
113
+
These three methods turn into query parameters that show up automatically in the OpenAPI document and Swagger UI. In every case the **array key is the public API name** and `setColumn()` maps it to the real database column, so the API stays stable even if the schema changes.
114
+
115
+
```php
116
+
use Binaryk\LaravelRestify\Filters\SearchableFilter;
117
+
use Binaryk\LaravelRestify\Filters\MatchFilter;
118
+
use Binaryk\LaravelRestify\Filters\SortableFilter;
-**`setColumn()`** decouples the API name (the array key) from the DB column. e.g. expose `systemName` while filtering on `sysName`.
149
+
-**`MatchFilter::setType()`** (`text`, `bool`, `integer`, `datetime`, …) drives both how the value is cast in the query and the parameter's type in the OpenAPI document.
150
+
- A simple `public static array $search = ['hostname', 'ip'];` is a shorthand alternative to `searchables()` when you only need plain column matching.
151
+
152
+
## Relationships (`related()`)
153
+
154
+
Declare related resources in `related()`. Each entry maps an array key to an *eager field* pointing at another repository:
|`BelongsTo`| the model holds the foreign key (to-one) ||
176
+
|`HasMany`| the related rows hold the foreign key (to-many) ||
177
+
|`BelongsToMany`| many-to-many through a pivot table |`attach` / `sync` / `detach`|
178
+
179
+
This automatically exposes:
180
+
181
+
-`GET /api/v1/devices/{id}/ports` list a relation (always a paginated list, even for `BelongsTo`)
182
+
-`?include=ports,location` embed relations inline on the parent
183
+
- for `BelongsToMany`: `POST .../attach/{rel}`, `POST .../sync/{rel}`, `POST .../detach/{rel}`
184
+
185
+
### The three identifiers (read this before adding relations)
186
+
187
+
`HasMany::make($name, TargetRepository::class)` sets two values, and a third is derived. Mixing them up causes `403`/`404`s, so it's worth understanding:
188
+
189
+
| Identifier | Source | Controls |
190
+
|---|---|---|
191
+
|**`relation`**| the `make()` first arg | the Eloquent method called to load data (`$model->{relation}()`) |
192
+
|**`attribute`**| the `make()` first arg | the `attach`/`sync`/`detach` URL segment |
193
+
|**segment / `label`**| auto-set to the **target repository's `uriKey`**| the **`GET` relationship** URL segment |
194
+
195
+
Restify resolves a nested route `/{parent}/{id}/{segment}` by requiring `{segment}` to (a) be a registered repository `uriKey`**and** (b) match a parent relation's `getAttribute()` (`= label ?? attribute`). The base class `App\Restify\Repository::collectRelated()` keeps these in sync for you it pins every relation's `label` to the target repository's `uriKey`. The practical consequences:
196
+
197
+
- The `GET` relationship segment is **always the target repository's `uriKey`**, e.g. `DeviceOutageRepository::uriKey() === 'device-outages'` → `GET /api/v1/devices/{id}/device-outages`.
198
+
- The **target model's repository is the single source of truth** for its public name. Rename it once via [`uriKey`](#resource-name--urls-urikey) and every relationship that points to it updates automatically no `->label()` calls scattered across parent repositories.
199
+
- You normally just declare the relation with its Eloquent method name; the URL "just works".
200
+
201
+
### When the Eloquent method name isn't URL-friendly
202
+
203
+
If the model's relationship method is camelCase (e.g. `devicesOwned()`), pass the kebab-case slug as the first argument (so `attach` URLs stay clean) and point `relation` at the real method with `tap()`:
static fn ($f) => $f->relation = 'devicesOwned', // the actual Eloquent method
209
+
),
210
+
```
211
+
212
+
For the example above:
213
+
214
+
-`GET /api/v1/users/{id}/devices` lists them (segment = `DeviceRepository::uriKey()` = `devices`).
215
+
-`POST /api/v1/users/{id}/attach/devices-owned` attaches (segment = `attribute` = `devices-owned`, bridged to `devicesOwned()` by the `RestifyAttachRelationResolver` middleware).
216
+
217
+
### Missing parent
218
+
219
+
A relationship request whose parent id doesn't exist (e.g. `/api/v1/devices/999999/ports`) returns `404` via the `EnsureRelatedParentExists` middleware rather than a 500.
220
+
221
+
## Resource name & URLs (`uriKey`)
222
+
223
+
Every repository has a **`uriKey`** the slug used both for its own collection path and for any relationship that points at it.
224
+
225
+
-**Default:** kebab-case plural of the class name minus `Repository``PortRepository → ports`, `DeviceOutageRepository → device-outages`.
226
+
-**Override** on the owning repository:
227
+
228
+
```php
229
+
public static $uriKey = 'device-outages';
230
+
```
231
+
232
+
Because relationship segments are derived from the *target* repository's `uriKey` (see above), this is the **single place** to set a resource's public name changing it moves `/api/v1/<uriKey>` and every `…/{id}/<uriKey>` reference at once.
233
+
82
234
## Step 2: Create or Update the Policy
83
235
84
236
Restify checks a `allowRestify()` method on the model's policy to determine if the resource should be accessible at all.
@@ -176,8 +328,10 @@ After registration, these endpoints are automatically available:
176
328
| POST |`/api/v1/locations`| Create (if fields are writable and policy allows) |
177
329
| PUT/PATCH |`/api/v1/locations/{id}`| Update |
178
330
| DELETE |`/api/v1/locations/{id}`| Delete |
331
+
| GET |`/api/v1/locations/{id}/<relation>`| List a relation declared in `related()`|
332
+
| POST |`/api/v1/locations/{id}/attach/<relation>`| Attach (for `BelongsToMany` relations) |
179
333
180
-
Plus search, filters, bulk operations, and nested resource support all provided by Restify automatically.
334
+
Plus search, filters, bulk operations, and nested resource support all provided by Restify automatically. The OpenAPI document at `/api/v1/openapi.json` updates itself from this configuration no regeneration step.
181
335
182
336
## Access Control Patterns
183
337
@@ -230,8 +384,11 @@ public static function indexQuery(RestifyRequest $request, Builder|Relation $que
230
384
| What | Path |
231
385
|------|------|
232
386
| Repositories |`app/Restify/`|
233
-
| Base repository |`app/Restify/Repository.php`|
387
+
| Base repository (relationship naming, action gating) |`app/Restify/Repository.php`|
234
388
| Service provider |`app/Providers/RestifyServiceProvider.php`|
0 commit comments