PHPackages                             tcds-io/laravel-prince - 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. tcds-io/laravel-prince

ActiveLibrary[API Development](/categories/api)

tcds-io/laravel-prince
======================

A Simple way to create rest API resources for your Laravel models.

1.6.1(1mo ago)028MITPHPPHP &gt;=8.4CI passing

Since Feb 25Pushed 1mo agoCompare

[ Source](https://github.com/tcds-io/laravel-prince)[ Packagist](https://packagist.org/packages/tcds-io/laravel-prince)[ RSS](/packages/tcds-io-laravel-prince/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (8)Dependencies (12)Versions (10)Used By (0)

Prince! Your Laravel Model API
==============================

[](#prince-your-laravel-model-api)

> "Dearly beloved, we are gathered here today to get through this thing called life." — Prince

Turn any Eloquent model into a fully working REST API — no controllers, no form requests, no manual routes.

```
ModelResourceBuilder::create(userPermissions: fn() => $user->permissions)
    ->resource(Invoice::class)
    ->resource(Product::class)
    ->routes();
```

---

What you get
------------

[](#what-you-get)

For every registered model the following routes are created automatically, using the model's `$table` as the URL segment:

MethodPathAction`GET``/invoices`Paginated list`GET``/invoices/_schema`Column schema + nested resource names`GET``/invoices/{id}`Single record (+ embedded nested lists)`POST``/invoices`Create`PATCH``/invoices/{id}`Update`DELETE``/invoices/{id}`DeleteAnd when global search is enabled:

MethodPathAction`GET``/search`Full-text search across all opted-in resources---

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

[](#installation)

```
composer require tcds-io/laravel-prince
```

No configuration needed — call it directly from your routes file.

---

Usage
-----

[](#usage)

Register your resources inside `routes/api.php`:

```
use Tcds\Io\Prince\ModelResourceBuilder;

Route::prefix('/api/backoffice')->group(function () {
    ModelResourceBuilder::create(userPermissions: fn() => $request->user()->permissions)
        ->resource(Invoice::class, globalSearch: true)
        ->resource(Product::class, globalSearch: true)
        ->routes();
});
```

The package reads each model's `$table` and `$casts` properties to determine route prefixes and column types — nothing to configure.

---

Nested resources
----------------

[](#nested-resources)

Register sub-resources via a callback. Nested routes are scoped to the parent automatically, and the parent's FK is inferred from the table name (`invoice_id` for `invoices`):

```
ModelResourceBuilder::create(userPermissions: fn() => $user->permissions)
    ->resource(
        model: Invoice::class,
        resources: fn(ModelResourceBuilder $b) => $b
            ->resource(InvoiceItem::class),
    )
    ->routes();
```

This registers the full nested route set under `/invoices/{invoiceId}/items`, with every request validated against the parent's existence.

### GET response includes inner lists

[](#get-response-includes-inner-lists)

`GET /invoices/{id}` automatically embeds each registered nested resource as an inner list — **`$with` eager loads on the model are ignored**, keeping the API shape fully controlled by what you register:

```
{
  "data": {
    "id": 1,
    "title": "November invoice",
    "amount": 299.00,
    "items": [
      { "id": 10, "description": "Widget A", "price": 49.00, "_resource": "/invoices/1/items/10" },
      { "id": 11, "description": "Widget B", "price": 99.00, "_resource": "/invoices/1/items/11" }
    ]
  },
  "meta": {
    "resource": "invoices",
    "schema": [...],
    "resources": ["items"]
  }
}
```

---

List responses
--------------

[](#list-responses)

Every item in a paginated list includes a `_resource` field — the direct URL to that record, including any outer route prefix and parent path for nested resources:

```
{
  "data": [
    { "id": 1, "title": "Invoice A", "_resource": "/api/backoffice/invoices/1" },
    { "id": 2, "title": "Invoice B", "_resource": "/api/backoffice/invoices/2" }
  ],
  "meta": {
    "resource": "invoices",
    "schema": [...],
    "current_page": 1,
    "per_page": 10,
    "total": 2,
    "last_page": 1
  }
}
```

### Pagination

[](#pagination)

Control page size with `?limit=N` (default `10`, max `100`):

```
GET /invoices?limit=25
GET /invoices?limit=25&page=2

```

---

Filtering
---------

[](#filtering)

### Full-text search

[](#full-text-search)

`?search=value` matches against all non-datetime columns using OR. Operators are inferred from the value:

```
GET /invoices?search=acme          → exact match across all text columns
GET /invoices?search=%acme%        → LIKE match

```

### Column filter

[](#column-filter)

`?{column}=value` filters on a specific column. The same operator inference applies:

```
GET /invoices?title=%acme%         → LIKE
GET /invoices?amount=>100          → greater than 100
GET /invoices?amount=N``>`integer, number, datetime`=`integer, number, datetime`permissions)
    ->resource(Invoice::class, globalSearch: true)
    ->resource(Product::class, globalSearch: true)
    ->resource(Customer::class)          // excluded from search
    ->routes();
```

```
GET /search?q=acme
GET /search?q=%acme%    → LIKE

```

```
{
  "data": [
    { "id": 1, "description": "Acme Corp", "resource": "invoices", "link": "/api/backoffice/invoices/1" },
    { "id": 7, "description": "Acme Widget", "resource": "products", "link": "/api/backoffice/products/7" }
  ]
}
```

Each result has `id`, `description` (first matching text column), `resource` (table name), and `link` (full URL including any outer route prefix). Each record appears at most once per resource even when multiple text columns match.

---

Permissions
-----------

[](#permissions)

### User permissions

[](#user-permissions)

A closure returning the permissions the current user holds. Evaluated per request — so it runs after authentication middleware, can read the request, and supports any auth strategy. Shared across all resources in the builder — pass once, applied everywhere:

```
ModelResourceBuilder::create(userPermissions: fn() => $request->user()->permissions)
    ->resource(Invoice::class)
    ->resource(Product::class)
    ->routes();
```

Because it's a closure, you can use any source:

```
// From the authenticated user
fn() => Auth::user()?->permissions ?? []

// From a gate/policy check
fn() => Gate::allows('admin') ? ['invoices:read', 'invoices:write'] : []

// Hard-coded (e.g. for public read-only endpoints)
fn() => ['invoices:read']
```

### Resource permissions

[](#resource-permissions)

The permission string each action *requires*. Defaults to the strings below; override per resource when your app uses different permission names:

ActionDefault required permission`list``default-model:list``get``default-model:get``create``default-model:create``update``default-model:update``delete``default-model:delete````
ModelResourceBuilder::create(userPermissions: fn() => Auth::user()?->permissions ?? [])
    ->resource(
        model: Invoice::class,
        resourcePermissions: [
            'list'   => 'invoices:read',
            'get'    => 'invoices:read',
            'create' => 'invoices:write',
            'update' => 'invoices:write',
            'delete' => 'invoices:delete',
        ],
    )
    ->routes();
```

---

Type inference
--------------

[](#type-inference)

The package inspects each database column and applies the right PHP type automatically. `$casts` on your model takes priority over the raw DB type.

DB / cast typeAPI typeParsed as`bigint`, `integer``integer``(int)``decimal`, `float``number``(float)``datetime``datetime``Carbon``immutable_datetime``datetime``CarbonImmutable`Any `BackedEnum``enum``MyEnum::from(...)`Anything else`text`passthroughEnum values are automatically included in the schema:

```
{ "name": "status", "type": "enum", "values": ["draft", "active", "cancelled"] }
```

---

Error handling
--------------

[](#error-handling)

SituationHTTP responseRecord not found`404 Not Found`Missing required permission`403 Forbidden`Invalid value / constraint error`400 Bad Request` with error detail---

Actions
-------

[](#actions)

Register extra endpoints on a resource with `actions`. Use `ResourceAction::{method}()` — paths containing `{id}` are item-level (the record is resolved and injected automatically); all other paths are collection-level.

The `action` must be an **invokable class** (a class with `__invoke`). Laravel's IoC container resolves and calls it, so any type-hinted dependencies are injected automatically.

```
use Tcds\Io\Prince\ResourceAction;

ModelResourceBuilder::create(userPermissions: fn() => $user->permissions)
    ->resource(
        model: Invoice::class,
        resourcePermissions: [
            'list'   => 'invoices:read',
            'get'    => 'invoices:read',
            'create' => 'invoices:write',
            'update' => 'invoices:write',
            'delete' => 'invoices:delete',
        ],
        actions: [
            // Collection-level — POST /invoices/import
            ResourceAction::post(
                path: '/import',
                action: ImportInvoicesAction::class,
                permission: 'invoices:write',
            ),

            // Item-level — POST /invoices/{id}/send
            // The matching Invoice is resolved and injected; returns 404 if not found.
            ResourceAction::post(
                path: '/{id}/send',
                action: SendInvoiceAction::class,
                permission: 'invoices:send',
            ),

            // GET /invoices/{id}/pdf
            ResourceAction::get(
                path: '/{id}/pdf',
                action: InvoicePdfController::class,
                permission: 'invoices:read',
            ),
        ],
    )
    ->routes();
```

**Collection actions** (`/import`) are registered before `/{id}` routes so literal path segments are never captured as record IDs.

**Item actions** (`/{id}/send`) resolve the record from the database before calling the action. The model instance is injected by type — any parameter type-hinted with the model class receives it. The full Laravel IoC is available for additional injectables (`Request`, services, etc.). `permission` is optional; omit it to allow unauthenticated access to that action.

---

Events
------

[](#events)

Every CRUD operation fires a lifecycle event before and after the DB write. Register listeners via standard Laravel event dispatching — no extra configuration needed.

EventWhenSignature`ResourceCreating`before insert`(class-string $modelName, array $data)``ResourceCreated`after insert`(Model $model)``ResourceUpdating`before update`(Model $model, array $data)``ResourceUpdated`after update`(Model $model)``ResourceDeleting`before delete`(Model $model)``ResourceDeleted`after delete`(int|string $modelId)````
use Tcds\Io\Prince\Events\ResourceCreated;
use Tcds\Io\Prince\Events\ResourceCreating;

// Side effect — send notification after create
Event::listen(ResourceCreated::class, function (ResourceCreated $event): void {
    if ($event->model instanceof Invoice) {
        Notification::send($event->model->user, new InvoiceCreatedNotification($event->model));
    }
});

// Data mutation — slugify title before save
Event::listen(ResourceCreating::class, function (ResourceCreating $event): void {
    if (isset($event->data['title'])) {
        $event->data['slug'] = Str::slug($event->data['title']);
    }
});
```

`ResourceCreating` and `ResourceUpdating` implement `MutableDataEvent` — any changes to `$event->data` are applied to the actual DB write.

### Overriding default events

[](#overriding-default-events)

Override any event per resource by passing an `events` array keyed by lifecycle name. Unspecified keys keep their defaults:

```
use Tcds\Io\Prince\Events\ResourceCreating;

ModelResource::of(
    model: Invoice::class,
    events: [
        'creating' => InvoiceCreating::class, // replaces ResourceCreating
        'created'  => InvoiceCreated::class,  // replaces ResourceCreated
    ],
);
```

The custom event must expose a public mutable `$data` property to participate in data mutation:

```
use Tcds\Io\Prince\Events\MutableDataEvent;

class InvoiceCreating implements MutableDataEvent
{
    public function __construct(
        public readonly string $modelName,
        public array $data,
    ) {}
}
```

---

Custom URL segment
------------------

[](#custom-url-segment)

Override the URL segment with `segment` when you want a different path than the table name:

```
ModelResourceBuilder::create(userPermissions: fn() => $user->permissions)
    ->resource(model: Invoice::class, segment: 'bills')
    ->routes();
// Routes registered at /bills/... — table name remains invoices in meta/schema
```

---

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

[](#requirements)

- PHP `^8.4`
- Laravel `^12.0`

###  Health Score

44

—

FairBetter than 92% of packages

Maintenance91

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity57

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 ~5 days

Total

8

Last Release

44d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/c8ac1821d71c0ddddcc3494a165dc40cfa3e759a467d8a00956ceeae4b833bb3?d=identicon)[thiagocordeiro](/maintainers/thiagocordeiro)

---

Top Contributors

[![thiagocordeiro](https://avatars.githubusercontent.com/u/1073649?v=4)](https://github.com/thiagocordeiro "thiagocordeiro (31 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/tcds-io-laravel-prince/health.svg)

```
[![Health](https://phpackages.com/badges/tcds-io-laravel-prince/health.svg)](https://phpackages.com/packages/tcds-io-laravel-prince)
```

###  Alternatives

[darkaonline/l5-swagger

OpenApi or Swagger integration to Laravel

2.9k34.0M112](/packages/darkaonline-l5-swagger)[echolabsdev/prism

A powerful Laravel package for integrating Large Language Models (LLMs) into your applications.

2.3k388.3k10](/packages/echolabsdev-prism)[sburina/laravel-whmcs-up

WHMCS API client and user provider for Laravel

271.3k](/packages/sburina-laravel-whmcs-up)

PHPackages © 2026

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