Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Crud Laravel Package

orchid/crud

Orchid CRUD is a screen-based toolkit for building admin panels and CRUD interfaces in Laravel. Define screens, fields, and layouts in PHP to create customizable back-office apps with forms, filters, sorting, and actions, all within your Laravel codebase.

View on GitHub
Deep Wiki
Context7

Getting Started

orchid/crud is not a standalone package — it is the screen-generation layer that sits on top of orchid/platform. Every "setup" story starts with a working Orchid admin panel.

Prerequisites (do these once, outside this package):

composer require orchid/platform
php artisan orchid:install        # publishes config, migrations, admin guard
php artisan migrate
php artisan orchid:admin admin [email protected] password

Confirm you can log into /admin before touching CRUD. If that fails, CRUD will only add noise to the debugging.

Minimal path to your first screen:

composer require orchid/crud
php artisan orchid:resource Tag     # generates app/Orchid/Resources/TagResource.php

Then fill in the three methods that actually matter — $model, fields(), columns():

namespace App\Orchid\Resources;

use App\Models\Tag;
use Orchid\Crud\Resource;
use Orchid\Screen\Fields\Input;
use Orchid\Screen\TD;

class TagResource extends Resource
{
    public static $model = Tag::class;

    public function fields(): array
    {
        return [
            Input::make('name')->title('Name')->required(),
            Input::make('slug')->title('Slug')->required(),
        ];
    }

    public function columns(): array
    {
        return [
            TD::make('name')->sort()->filter(),
            TD::make('slug'),
            TD::make('created_at')->sort()
                ->render(fn (Tag $tag) => $tag->created_at?->toDateString()),
        ];
    }

    public function rules(\Illuminate\Database\Eloquent\Model $model): array
    {
        return [
            'name' => 'required|max:255',
            'slug' => ['required', \Illuminate\Validation\Rule::unique('tags', 'slug')->ignore($model)],
        ];
    }
}

Register the resourceful routes once, in routes/platform.php:

Route::resource('tags', \App\Orchid\Resources\TagResource::class);

Reload /admin and you have list, create, show, edit, and delete screens, plus a navigation entry and a permission-aware menu item. No controller, no Blade file, no route-per-action.

Where to look first, in this order:

  1. The generated resource class — it is the whole contract. Read Orchid\Crud\Resource in vendor/orchid/crud/src/Resource.php to see every overridable method (displayInNavigation, icon, group, sort, perPage, with, filters, actions, buttons, permission, screens, request, …). That file is your real documentation.
  2. vendor/orchid/crud/src/Screens/ — the default List/Create/Edit/View screens. Read them before you try to customise behaviour; most "how do I change X" questions are answered by seeing which method the screen calls.
  3. Orchid's own docs for Field and TD — CRUD does not re-document the field library, and the field library is where 80% of your day-to-day work lives.

Best first use case: an internal entity nobody wants to design — tags, categories, feature flags, webhook endpoints, support ticket statuses. Twenty lines of resource replaces a controller, two Blade views, a form request, and a nav entry. That is the value proposition, and it is worth proving on something disposable before you commit a roadmap to it.


Implementation Patterns

One resource = one entity's whole contract

The pattern that makes CRUD cheap is refusing to spread an entity across files. Fields, table columns, validation, filters, actions, navigation placement, and permissions all live in the resource. When a PM asks "can we add a status column and filter by it?", the answer is one diff in one file.

public function filters(): array
{
    return [
        Filter\Where::make('status'),
        Filter\Where::make('owner_id')->fromModel(Owner::class, 'name'),
    ];
}

public function with(): array
{
    return ['owner'];   // eager-load anything a column renders
}

public function perPage(): int
{
    return 50;
}

public function group(): ?string
{
    return 'Content';
}

Validation: branch on exists, push complexity into a request class

rules() receives the model, which is exactly what you need for unique rules on create vs update:

public function rules(Model $model): array
{
    return [
        'slug' => [
            'required',
            Rule::unique('posts', 'slug')->ignore($model),
        ],
        'published_at' => 'nullable|date|after:now',
    ];
}

When rules grow past a screenful — or you need custom messages/attribute names/authorization — move them to a resource request class and point at it:

public function request(): string
{
    return \App\Orchid\Requests\PostRequest::class;
}

Keep rules() trivial and delegate. Mixed sources of validation truth are how "why did this save with a blank slug" bugs are born.

Relations: trailing-dot fields, Saves does the rest

Orchid's Saves trait handles relation persistence when the field name carries the relation prefix. The generated CRUD screens already use it, so you get pivot sync for free as long as you name fields correctly:

Relation::make('tags.')
    ->title('Tags')
    ->fromManyToMany(Tag::class, 'name')
    ->applyScope('published'),

Simple foreign keys stay as plain selects; only many-to-many or has-many editing needs the trailing dot.

Actions: declarative visibility, real authorization

The default action set (view/edit/delete) comes from actions(). Override it to trim or extend, and use the 4.4.0 hide method for role/state-based visibility instead of hand-rolling conditional Blade:

public function actions(): array
{
    return [
        DeleteAction::make()
            ->hide(fn (Post $post) => $post->is_locked),
        ExportAction::make()
            ->hide(fn () => ! request()->user()?->hasAccess('platform.posts.export')),
    ];
}

For genuinely custom workflows, extend the package's Action base class, implement the handler, and declare the button/modal it renders. Keep the handler guarded — see the gotcha below.

Permissions: coarse on the resource, fine on the actions

permission() on the resource gates the whole screen set and is the right place for "only support leads can touch billing". Row-level and action-level gating belongs in hide() plus a real check in the handler. Two layers, deliberately:

public function permission(): ?string
{
    return 'platform.billing.manage';
}

Navigation shape without touching PlatformProvider

displayInNavigation(), group(), icon(), sort(), and title()/description() fully describe the menu entry. This is the single biggest time saver versus vanilla Orchid, where every screen is registered by hand in app/Orchid/PlatformProvider.php. Keep those methods cheap — see the performance gotcha.

Customising screens

When the default List screen isn't enough (bulk operations, exports, a custom layout), don't edit vendor files and don't abandon the resource. Subclass Orchid\Crud\Screens\ListScreen (or the relevant screen), add your command bar / buttons / layout, and map the default to yours via the resource's screens() method. You keep fields, columns, filters, and validation in one place and only diverge where you must.

Integration with the rest of the app

  • Put the CRUD resources in app/Orchid/Resources/ and keep models in app/Models/ — the resource is presentation/validation, the model owns business logic. Fire model events (saved, deleted) for side effects rather than stuffing them into resource hooks.
  • Seed factories for every CRUD-managed model and add a Pest feature test per resource: get the index route as an authorized user, post the store route with valid and invalid payloads, and assert 403 for an unauthorized role. Three tests per resource catches most regressions introduced by an Orchid bump.
  • Keep resource URIs unique and stable — route names derive from them, and permission keys usually follow the same convention.

Gotchas and Tips

hide() is not authorization. It removes a button from the DOM. It does not protect the endpoint. Anyone with the route can still POST the action. Every hide() closure needs a matching server-side check in the handler, the resource's permission(), or a policy. Treat hide() as information-disclosure hygiene and record it in access reviews as exactly that.

orchid/crud tracks orchid/platform's release cadence — version-lock them together. An Orchid major can change CRUD behaviour with no CRUD version bump. Pin both, upgrade both in the same PR, and put a standing yearly slot on the calendar for Laravel's major cycle. Laravel 12 support landed in 4.4.0, but CRUD cannot outrun its host: verify orchid/platform itself supports Laravel 12 before you commit to a framework upgrade date.

displayInNavigation() runs on every menu render. It has no cache of its own. A DB query or expensive can() check there is a per-request tax on every admin page, not just the resource's. Memoise or keep it to a hasAccess() call. Additionally: 4.4.0 fixed request()->user() being null in this method, which changes behaviour for guest-reachable, tenant-scoped, and impersonated admin routes. Write it null-safe (request()->user()?->...) and re-test those three flows after upgrading — they are the ones the fix touches.

Model $fillable/$guarded still governs persistence. The classic "my field renders but nothing saves" bug is a column missing from $fillable, or a field missing from fields() entirely (fields not declared are never saved). Check both before suspecting the package.

Field names and rule keys must agree. Keep them unprefixed and identical to the column. If you adopt a prefix convention (post.title), the keys in rules(), messages(), and attributes() must use the same prefix or validation silently passes.

TD::make(...)->sort() sorts by the column name, not by what ->render() prints. Sorting a relation-derived column (author.name) will not work without a join or a scope. Either render the raw foreign key with a filter, or accept an unsortable column.

Filters need a model that participates. ->filter() on a TD and entries in filters() require the model to use Orchid's filterable trait; without it you get an empty dropdown or a filter that does nothing. The failure mode is silent, so verify the filter actually narrows the query on first build.

->render() closures run once per row. Fine for ->toDateString() or ->statusLabel(). Not fine for lazy-loading a relation. Pair every relation-backed column with with() and watch the query count in Debugbar on a 50-row page before shipping.

Deleting is a hard delete by default. If the model uses SoftDeletes, the default action respects the model's delete(), but the UI will present it as permanent. Consider a restorable "Archive" action and a hide() on delete for roles that shouldn't hard-delete.

Debugging a 404 or a missing menu item:

  1. php artisan route:list --path=platform — is the route registered? If not, routes/platform.php isn't loading or the Route::resource line is missing.
  2. Menu item absent but route present? Check displayInNavigation(), permission(), and any cached routes/config (php artisan optimize:clear).
  3. Screen renders but is empty? Check columns() returns TD instances (not fields) and fields() returns Field instances (not TD).
  4. Save appears to work but nothing changed? Check $fillable, then rules() keys, then whether the field name matches the DB column.

Route naming collisions. Two resources on the same URI segment produce colliding route names and unpredictable action routing. Namespace related URIs (content.tags, content.posts) rather than registering two tags resources from different modules.

Don't edit vendor screens. Every customisation should be a subclass or a resource method. Patch files in vendor/ are the reason teams get stuck a Laravel major behind — and with a small contributor base and few downstream dependents, you are your own support contract. Budget a short internal README covering your resource conventions, your permission-key format, and your screen-override policy; that document is the actual mitigation for the expertise concentration risk.

Useful extension points, roughly in order of how often you'll reach for them: columns() render closures → with() for eager loading → filters() → actions() with hide() → a custom request class → screens() for a subclassed screen → a fully bespoke Orchid screen registered alongside, for the one page the resource abstraction genuinely doesn't fit.

Know when to step out. If a screen needs a bespoke layout, multi-step wizard, or real-time updates, build a normal Orchid screen next to the resources rather than contorting the resource API. The package's value is that conventional CRUD is nearly free; it is not a general-purpose UI framework, and fighting it costs more than the boilerplate it saved.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log