PHPackages                             afurgeri/laravel-crud - 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. afurgeri/laravel-crud

ActiveLibrary

afurgeri/laravel-crud
=====================

Declarative CRUD definitions and managers for Laravel applications.

v0.7.9(3w ago)02201MITPHPPHP ^8.3CI failing

Since Jul 16Pushed 1w agoCompare

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

READMEChangelog (1)Dependencies (17)Versions (67)Used By (1)

Laravel CRUD
============

[](#laravel-crud)

Declarative CRUD definitions, schema generation, filtering, sorting, validation, authorization hooks, and mutation managers for Laravel applications.

The package owns the reusable backend behavior. Resource-specific models, policies, controllers, routes, and frontend pages remain in the consuming application.

The CRUD managers use Laravel's Eloquent contracts and can therefore be used with SQL connections and the official `mongodb/laravel-mongodb` driver. MongoDB support is optional; installing the package does not require the MongoDB PHP extension.

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

[](#requirements)

- PHP 8.3+
- Laravel 13+

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

[](#installation)

```
composer require afurgeri/laravel-crud
```

The service provider is registered automatically through Laravel package discovery.

If Packagist is temporarily unavailable, Composer can install the same tagged release directly from GitHub:

```
{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/afurgeri/laravel-crud"
        }
    ]
}
```

Then require the desired tag:

```
composer require afurgeri/laravel-crud:^0.4
```

Remove the temporary VCS repository once Packagist is available again. The VCS fallback is only an installation workaround; it does not change the package or its version constraints.

Optional MongoDB integration
----------------------------

[](#optional-mongodb-integration)

Install `mongodb/laravel-mongodb` in the consuming application when a CRUD model should use MongoDB:

```
composer require mongodb/laravel-mongodb
```

Add the MongoDB connection to `config/database.php` in the consuming application:

```
'connections' => [
    // ...

    'mongodb' => [
        'driver' => 'mongodb',
        'dsn' => env('MONGODB_URI', 'mongodb://127.0.0.1:27017'),
        'database' => env('MONGODB_DATABASE', 'laravel'),
    ],
],
```

Configure the connection string and database name in `.env`:

```
MONGODB_URI="mongodb://127.0.0.1:27017"
MONGODB_DATABASE="laravel"
```

The `mongodb` PHP extension must be installed and enabled. Install it with PECL when it is not already available:

```
pecl install mongodb
```

The model must extend `MongoDB\Laravel\Eloquent\Model` and define a MongoDB connection or use the application's MongoDB default connection. When SQL is the default connection, declare the MongoDB connection on the model explicitly:

```
namespace App\Models;

use MongoDB\Laravel\Eloquent\Model;

class Citizen extends Model
{
    protected $connection = 'mongodb';

    protected $collection = 'citizens';
}
```

The CRUD managers continue using the same Eloquent API for pagination, filtering, sorting, eager loading, validation, and mutations.

### Supported operations

[](#supported-operations)

OperationSQLMongoDBCreate, update, delete, refreshSupportedSupportedProjection and sortingSupportedSupportedPaginationSupportedSupportedText search with `like`SQL `LIKE`MongoDB regular expressionDate filtersSQL date clausesBSON date rangesEager loadingSupportedSupported for supported relations`whereHas` relation filtersSupportedSupported for referenced relationsUnique validationUses the model connectionUses the model connection### Referenced relationships

[](#referenced-relationships)

Standard referenced relationships use the same Eloquent declarations. For example, a city can own many citizens while each citizen belongs to one city:

```
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use MongoDB\Laravel\Eloquent\Model;

class City extends Model
{
    protected $connection = 'mongodb';

    protected $collection = 'cities';

    /**
     * @return HasMany
     */
    public function citizens(): HasMany
    {
        return $this->hasMany(Citizen::class, 'city_id', '_id');
    }
}

class Citizen extends Model
{
    protected $connection = 'mongodb';

    protected $collection = 'citizens';

    /**
     * @return BelongsTo
     */
    public function city(): BelongsTo
    {
        return $this->belongsTo(City::class, 'city_id', '_id');
    }
}
```

The integration suite verifies both directions, eager loading, and CRUD relation filters with real MongoDB documents. MongoDB models use `_id` internally while exposing the usual Eloquent `id` attribute.

### Integration tests

[](#integration-tests)

MongoDB integration tests are separate from the default SQL suite, so SQL-only installations remain unchanged. From the package repository, install the optional test dependency, enable the MongoDB PHP extension, start MongoDB, and run:

```
composer require mongodb/laravel-mongodb:^5.8 --dev
composer test:mongodb
```

The test suite uses MongoDB 8 in CI and verifies the CRUD managers against a real MongoDB server. MongoDB-specific indexes, embedded relations, and aggregation pipelines remain application-level concerns.

### Scope and limits

[](#scope-and-limits)

- SQL and MongoDB models can coexist in the same application.
- A model's explicit connection is respected by CRUD unique validation.
- Referenced `hasMany` and `belongsTo` relationships are supported when both models use MongoDB.
- Embedded relationships such as `embedsMany` and `embedsOne` have different document semantics and are not covered by the generic CRUD contract.
- SQL and MongoDB do not share one atomic transaction. Use transactions within the database that owns the operation.
- Compatibility does not imply equivalent performance. Add MongoDB indexes for filters, sorts, and relationship keys, then benchmark with production-shaped data.

Quick path
----------

[](#quick-path)

For each CRUD resource:

1. Add the `HasCrudDefinition` contract and concern to the model.
2. Create a definition class implementing `CrudDefinition`.
3. Declare columns, fields, and optional filters in the definition.
4. Inject the CRUD managers into the resource controller.
5. Render the generated schema and paginated records in the frontend.

The `Role` and `User` resources in the companion application follow this structure.

Generator command
-----------------

[](#generator-command)

Install the generic frontend integration once per consuming application:

```
php artisan crud:install
```

This copies the reusable Vue components and TypeScript definitions to `resources/js`. Use `--force` only when you intentionally want to replace local changes:

```
php artisan crud:install --force
```

The package also registers a project scaffold command through Laravel package discovery:

```
php artisan make:crud Product --module=Catalog
```

Omit `--module` to generate the resource in the consuming application's default Laravel locations:

```
php artisan make:crud Product
```

Generate a MongoDB model instead of an SQL model and migration with:

```
php artisan make:crud Product --module=Catalog --database=mongodb
```

This also works without a module:

```
php artisan make:crud Product --database=mongodb
```

When the consuming application also uses `afurgeri/laravel-rbac`, generate permission constants, an RBAC policy, and an idempotent permission seeder:

```
php artisan make:crud Product --module=Catalog --rbac
```

The generated permissions use the resource name (`products.view`, `products.create`, `products.update`, and `products.delete`). Run the generated permission seeder before `AdminRoleSeeder` when the admin role should receive the new permissions automatically.

Use a singular entity name such as `Person`. Laravel resource routes singularize the URI parameter (`/people/{person}`), and the generated controller uses that same parameter for implicit model binding. Passing a plural entity name is supported, but singular names keep the generated model and controller names conventional.

It generates the starting files for a complete resource:

- SQL migration when `--database=mysql` is selected;
- Eloquent model;
- `CrudDefinition`;
- controller;
- policy;
- factory;
- CRUD definition test;
- Inertia/Vue index page;
- module provider and routes when the module is new;
- application routes in `routes/web.php` when no module is supplied.

MongoDB generation does not create a SQL migration. Define MongoDB indexes in the consuming application, add navigation entries, and keep generated record IDs typed as strings in application-specific frontend code.

Generated pages include previous/next pagination controls. CRUD definitions use 10 items per page by default; implement `HasDefaultCrudPageSize` to override that value:

```
use Modules\Crud\Contracts\HasDefaultCrudPageSize;

class ProductCrudDefinition implements CrudDefinition, HasDefaultCrudPageSize
{
    public function defaultPageSize(): int
    {
        return 25;
    }
}
```

The request may still provide `per_page`; values are normalized to the supported range of 1 to 100. Sorting, search, and filters are preserved while changing pages and reset pagination when changed.

The command is intentionally opinionated around the module structure used by this project and Laravel + Inertia/Vue applications. It is not a generic model generator. After generation, review the placeholder `name` field, authorization rules, navigation, relationships, and frontend slots.

### Options

[](#options)

```
php artisan make:crud Product \
    --module=Catalog \
    --table=products \
    --database=mysql \
    --force
```

- `--module` is optional and uses a StudlyCase module name. When omitted, the command generates the model, definition, controller, and policy under `app/`, adds the route to `routes/web.php`, and does not modify Composer autoloading or service providers.
- `--table` overrides the default snake\_case plural table name.
- `--database` selects `mysql` (default) or `mongodb`. MongoDB generation creates a MongoDB model and leaves collection indexes to the consuming application.
- `--rbac` requires `afurgeri/laravel-rbac` and generates a resource-local permissions class, policy, and idempotent permission seeder. It is opt-in so CRUD remains usable without RBAC.
- `--force` allows overwriting generated files that already exist.

When the project has Wayfinder installed, the command regenerates route helpers automatically. Without Wayfinder, generation still succeeds and prints a warning. Pint is also used when available, but is not required for generation.

The command updates the application's Composer PSR-4 mapping and `bootstrap/providers.php` only when it creates a new module. Existing modules only receive the new resource route and generated files.

1. Connect the model
--------------------

[](#1-connect-the-model)

The model points to its definition through a small bridge. This keeps CRUD configuration out of the Eloquent model while allowing controllers to resolve it consistently.

```
namespace App\Models;

use App\Crud\ProductCrudDefinition;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Modules\Crud\Concerns\HasCrudDefinition;
use Modules\Crud\Contracts\HasCrudDefinition as HasCrudDefinitionContract;

#[Fillable(['name', 'sku', 'price'])]
class Product extends Model implements HasCrudDefinitionContract
{
    use HasCrudDefinition;

    public static function crudDefinition(): string
    {
        return ProductCrudDefinition::class;
    }
}
```

`makeCrudDefinition()` is provided by the concern and resolves the definition through Laravel's container:

```
$definition = Product::makeCrudDefinition();
```

The definition class must be instantiable by the container. Constructor injection is supported when a definition needs an application service.

2. Define the resource
----------------------

[](#2-define-the-resource)

A definition implements `CrudDefinition` and returns the model plus the metadata used by the backend and frontend.

```
namespace App\Crud;

use Illuminate\Database\Eloquent\Model;
use Modules\Crud\CrudColumn;
use Modules\Crud\CrudDefinition;
use Modules\Crud\CrudField;
use Modules\Crud\Concerns\AuthorizesViaGate;
use Modules\Crud\Contracts\AuthorizesCrudIndex;
use Modules\Crud\Contracts\AuthorizesCrudMutations;
use Modules\Crud\Contracts\HasDefaultCrudSort;
use App\Models\Product;

class ProductCrudDefinition implements
    CrudDefinition,
    AuthorizesCrudIndex,
    AuthorizesCrudMutations,
    HasDefaultCrudSort
{
    use AuthorizesViaGate;

    /** @return class-string */
    public function model(): string
    {
        return Product::class;
    }

    public function title(): string
    {
        return __('Products');
    }

    public function description(): ?string
    {
        return __('Manage products.');
    }

    public function emptyLabel(): ?string
    {
        return __('No products found.');
    }

    public function columns(): array
    {
        return [
            CrudColumn::make('id')->sortable(),
            CrudColumn::make('name')->sortable()->searchable(),
            CrudColumn::make('sku')->sortable()->searchable(),
            CrudColumn::make('price')->sortable(),
        ];
    }

    public function fields(): array
    {
        return [
            CrudField::make('name', ['required', 'string', 'max:255']),
            CrudField::make('sku', ['required', 'string', 'max:50'])->unique(),
            CrudField::make('price', ['required', 'numeric', 'min:0']),
        ];
    }

    public function defaultSortColumn(): string
    {
        return 'name';
    }

    public function defaultSortDirection(): string
    {
        return 'asc';
    }
}
```

### Definition methods

[](#definition-methods)

MethodPurpose`model()`Eloquent model class queried and mutated by the managers.`title()`Resource title sent in the schema.`description()`Optional explanatory text.`emptyLabel()`Optional empty-table message.`columns()`Columns exposed by the index.`fields()`Fields validated and accepted by create/update mutations.Columns
-------

[](#columns)

```
CrudColumn::make('name')
    ->sortable()
    ->searchable();

CrudColumn::make('internal_code')->hidden();

CrudColumn::make('permission_ids')->computed();
```

- `sortable()` allows the column in the requested `sort` parameter.
- `searchable()` includes the column in the text search.
- A searchable model key is matched exactly. MongoDB keys are converted to `ObjectId` values when possible; invalid ObjectId input produces no key match.
- `hidden()` keeps the column out of the generated schema.
- `computed()` marks a value that is added by the controller and must not be selected, sorted, or searched as a database column.

Fields and validation
---------------------

[](#fields-and-validation)

Fields define both the generated form metadata and the validation rules used by `CrudMutationManager`.

```
CrudField::make('email', ['required', 'email', 'max:255'])
    ->email()
    ->unique();

CrudField::make('password', ['required', 'string', 'min:8'])
    ->password()
    ->confirmed()
    ->createOnly();
```

- `unique()` adds a database uniqueness rule and ignores the current model during update.
- `unique('external_id')` validates a different database column.
- `email()` and `password()` select the frontend input type.
- `confirmed()` adds Laravel's `confirmed` rule and exposes the confirmation hint in the schema.
- `createOnly()` hides the field during update and excludes it from update validation.
- `rules([...])` replaces the field's validation rules.

The model must still define `$fillable` or the equivalent Laravel model attribute for every mutable field.

Sorting
-------

[](#sorting)

Implement `HasDefaultCrudSort` to define the initial ordering:

```
use Modules\Crud\Contracts\HasDefaultCrudSort;

public function defaultSortColumn(): string
{
    return 'name';
}

public function defaultSortDirection(): string
{
    return 'asc';
}
```

Only columns declared as sortable can be requested by the client. Invalid sort columns are rejected by the index manager; invalid directions are normalized to `asc`.

Filters
-------

[](#filters)

Implement `HasCrudFilters` and return `CrudFilter` instances:

```
use Modules\Crud\Contracts\HasCrudFilters;
use Modules\Crud\CrudFilter;

class ProductCrudDefinition implements CrudDefinition, HasCrudFilters
{
    public function filters(): array
    {
        return [
            CrudFilter::make('status')->select([
                'active' => 'Active',
                'archived' => 'Archived',
            ])->clearable(),

            CrudFilter::make('created_from', 'created_at')
                ->date()
                ->operator('>=')
                ->range('created_at')
                ->default(fn (): string => now()->startOfMonth()->toDateString())
                ->maxDate(fn (): string => now()->toDateString()),

            CrudFilter::make('created_to', 'created_at')
                ->date()
                ->operator('string('sort')->toString() ?: null;
    $direction = $request->string('direction', 'asc')->toString();
    $search = $request->string('search')->toString() ?: null;
    $filters = $request->array('filters');

    /** @var LengthAwarePaginator $products */
    $products = $index->paginate(
        definition: $definition,
        page: $request->integer('page', 1),
        perPage: $request->integer('per_page', 15),
        sort: $sort,
        direction: $direction,
        search: $search,
        filters: $filters,
    );

    $products->through(fn (Product $product): array => [
        'id' => $product->id,
        'name' => $product->name,
        'sku' => $product->sku,
    ]);

    return Inertia::render('products/Index', [
        'crud' => $schema->for($definition, 'products', $sort, $direction, $search, $filters),
        'products' => $products,
    ]);
}
```

`CrudSchemaManager::for()` returns the metadata needed by a generic frontend: columns, fields, sort state, search state, filters, labels, and resolved select options.

### Pagination hooks

[](#pagination-hooks)

Implement `HasCrudPaginationHooks` and use `HandlesCrudPaginationHooks` when a definition needs to adjust the query before pagination or transform the returned items:

```
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Modules\Crud\Concerns\HandlesCrudPaginationHooks;
use Modules\Crud\Contracts\HasCrudPaginationHooks;

class ProductCrudDefinition implements CrudDefinition, HasCrudPaginationHooks
{
    use HandlesCrudPaginationHooks;

    /** @param Builder $query */
    public function beforePaginate(Builder $query): void
    {
        // Add resource-specific query constraints here.
    }

    /** @param LengthAwarePaginator $paginator */
    public function afterPaginate(LengthAwarePaginator $paginator): void
    {
        $paginator->through(fn (Model $product): array => [
            'id' => $product->id,
            'name' => $product->name,
        ]);
    }
}
```

The manager runs `beforePaginate()` after authorization, search, filters, and sorting, but before executing the query. It runs `afterPaginate()` after the paginator has been created. The paginator metadata is preserved when items are transformed with `through()`.

### Create, update, and delete

[](#create-update-and-delete)

Inject `CrudMutationManager` and pass the definition to the corresponding operation:

```
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Modules\Crud\CrudMutationManager;

public function store(Request $request, CrudMutationManager $mutations): RedirectResponse
{
    $mutations->create(Product::makeCrudDefinition(), $request->all());

    return to_route('products.index');
}

public function update(
    Request $request,
    Product $product,
    CrudMutationManager $mutations,
): RedirectResponse {
    $mutations->update($product, Product::makeCrudDefinition(), $request->all());

    return to_route('products.index');
}

public function destroy(Product $product, CrudMutationManager $mutations): RedirectResponse
{
    $mutations->delete($product, Product::makeCrudDefinition());

    return to_route('products.index');
}
```

The mutation manager validates only the fields declared by the definition, applies policy authorization when configured, fills the model, and persists it. Relationship synchronization and resource-specific validation can remain in the resource controller, or use the optional mutation hook contract when the work must happen after CRUD authorization.

### Mutation hooks

[](#mutation-hooks)

Implement `HasCrudMutationHooks` and use `HandlesCrudMutationHooks` when a definition needs lifecycle callbacks:

```
use Illuminate\Database\Eloquent\Model;
use Modules\Crud\Concerns\HandlesCrudMutationHooks;
use Modules\Crud\Contracts\HasCrudMutationHooks;

class ProductCrudDefinition implements CrudDefinition, HasCrudMutationHooks
{
    use HandlesCrudMutationHooks;

    /** @param array $data */
    public function afterUpdate(Model $model, array $data): void
    {
        $model->tags()->sync($data['tag_ids'] ?? []);
    }
}
```

The manager runs hooks in this order:

1. Operation and authorization checks.
2. Validation and delete guards.
3. The `beforeCreate`, `beforeUpdate`, or `beforeDelete` hook.
4. Persistence.
5. The corresponding `afterCreate`, `afterUpdate`, or `afterDelete` hook.

The create and update hooks receive the original input array in addition to the model. Delete hooks receive only the model because `delete()` does not accept input data. An `after` hook runs only after the mutation succeeds. The optional trait supplies no-op implementations for hooks that are not needed.

Routes and frontend
-------------------

[](#routes-and-frontend)

Use normal Laravel resource routes in the application:

```
Route::resource('products', ProductController::class)
    ->only(['index', 'store', 'update', 'destroy']);
```

The package has no Vue or React dependency. A consuming application can pass the `crud` schema to its own generic table/form components. The companion scaffold uses a `CrudPage` component with this contract:

```
return Inertia::render('products/Index', [
    'crud' => $schema->for($definition, 'products', $sort, $direction, $search, $filters),
    'products' => $products,
]);
```

Resource-specific UI, such as role-permission checkboxes or user-role selectors, should be implemented as slots or dedicated components in the consuming application rather than added to the backend package.

Local development
-----------------

[](#local-development)

The scaffold application consumes this repository from `modules/Crud` through a Composer path repository. Changes made in that directory are immediately available after Composer regenerates the autoloader:

```
composer dump-autoload
```

The package itself is maintained in its own repository:

```
https://github.com/afurgeri/laravel-crud

```

Testing guidance
----------------

[](#testing-guidance)

Definitions should be tested with fake Eloquent models and test-created tables. Test the definition schema, validation, authorization, filters, and mutation behavior independently from application-specific entities.

The package includes its own Pest and Testbench suite:

```
composer install
composer lint
composer test
```

GitHub Actions runs the manifest validation, formatting check, and tests against PHP 8.3 using both the lowest and stable dependency sets.

API checklist for a new entity
------------------------------

[](#api-checklist-for-a-new-entity)

- Model implements `HasCrudDefinitionContract` and uses `HasCrudDefinition`.
- Definition implements `CrudDefinition`.
- Every mutable field is declared in `fields()` and fillable on the model.
- Database columns intended for sort/search are explicitly declared in `columns()`.
- Computed or relationship-derived values are marked `computed()` and serialized by the controller.
- Policies are configured before enabling `AuthorizesViaGate`.
- Relationship data used per row is declared through `EagerLoadsCrudRelations`.
- Filters are declared through `HasCrudFilters` and use a valid operator.
- Resource-specific relationships are synchronized in the controller after the base mutation.
- The controller passes the schema and paginator to the frontend page.

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance97

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

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

66

Last Release

22d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/106419289?v=4)[Alejandro Furgeri](/maintainers/afurgeri)[@afurgeri](https://github.com/afurgeri)

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/afurgeri-laravel-crud/health.svg)

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

###  Alternatives

[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45945.2k1](/packages/pressbooks-pressbooks)[laravel/ai

The official AI SDK for Laravel.

1.1k6.4M361](/packages/laravel-ai)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

255.2k](/packages/aedart-athenaeum)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

80732.6M272](/packages/laravel-mcp)[yajra/laravel-oci8

Oracle DB driver for Laravel via OCI8

8793.4M28](/packages/yajra-laravel-oci8)[illuminate/queue

The Illuminate Queue package.

20433.5M1.9k](/packages/illuminate-queue)

PHPackages © 2026

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