name: moonshine-resources-v3 description: Use when creating ModelResource, configuring CRUD operations, working with tables/forms/detail pages, adding filters, search, pagination, events, buttons, query modification, import/export, or metrics in MoonShine v3.
MoonShine v3 -- ModelResource
Overview
ModelResource is the primary building block for admin panel sections backed by Eloquent models. It extends CrudResource and provides full CRUD functionality: index listing, create/edit forms, detail views, filtering, search, pagination, events, authorization, import/export, and metrics.
Creating a Resource
php artisan moonshine:resource Post
This generates a resource class in app/MoonShine/Resources/ and registers it in MoonShineServiceProvider.
Core Properties
namespace App\MoonShine\Resources;
use App\Models\Post;
use MoonShine\Laravel\Resources\ModelResource;
/**
* @extends ModelResource<Post>
*/
class PostResource extends ModelResource
{
protected string $model = Post::class;
protected string $title = 'Posts';
protected array $with = ['category']; // eager load
protected string $column = 'id'; // display column for breadcrumbs/relations
protected ?string $alias = null; // custom URL alias
}
Declaring Fields per Page
Use indexFields(), formFields(), and detailFields() to define fields for each CRUD page.
For field types, configuration, and relationship fields, see the
moonshine-fields-v3skill.
use MoonShine\UI\Components\Layout\Box;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Text;
protected function indexFields(): iterable
{
return [
ID::make(),
Text::make('Title'),
];
}
protected function formFields(): iterable
{
return [
Box::make([
ID::make(),
Text::make('Title'),
Text::make('Subtitle'),
]),
];
}
protected function detailFields(): iterable
{
return [
Text::make('Title', 'title'),
Text::make('Subtitle'),
];
}
Pages-Based Resource (Alternative)
Instead of defining fields in the resource, you can generate separate page classes:
use App\MoonShine\Pages\Post\PostIndexPage;
use App\MoonShine\Pages\Post\PostFormPage;
use App\MoonShine\Pages\Post\PostDetailPage;
protected function pages(): array
{
return [
PostIndexPage::class,
PostFormPage::class,
PostDetailPage::class,
];
}
Each page class extends IndexPage, FormPage, or DetailPage and has its own fields() method. See references/crud-pages.md for full customization details.
Table Configuration
use MoonShine\Support\Enums\SortDirection;
use MoonShine\Support\Enums\ClickAction;
class PostResource extends ModelResource
{
protected string $sortColumn = 'created_at';
protected SortDirection $sortDirection = SortDirection::DESC;
// Pagination
protected int $itemsPerPage = 25;
protected bool $simplePaginate = false; // use simple pagination
protected bool $cursorPaginate = false; // use cursor pagination
protected bool $usePagination = true; // set false to disable
// Async mode (enabled by default)
protected bool $isAsync = true;
protected bool $isLazy = false; // lazy-load table data
// Display
protected bool $stickyTable = false; // sticky table header
protected bool $stickyButtons = false; // sticky action buttons column
protected bool $columnSelection = false; // let users toggle columns
// Click action on table row
protected ?ClickAction $clickAction = null;
// Options: ClickAction::SELECT, ClickAction::DETAIL, ClickAction::EDIT
}
Table Attributes
use Closure;
use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;
protected function trAttributes(): Closure
{
return fn(?DataWrapperContract $data, int $row) => [
'data-tr' => $row,
];
}
protected function tdAttributes(): Closure
{
return fn(?DataWrapperContract $data, int $row, int $cell) => [
'width' => '20%',
];
}
Custom thead/tbody/tfoot
use MoonShine\UI\Collections\TableCells;
use MoonShine\UI\Collections\TableRows;
use MoonShine\UI\Components\Table\TableRow;
protected function tfoot(): null|TableRowsContract|Closure
{
return static function(?TableRowContract $default, TableBuilder $table) {
$cells = TableCells::make();
$cells->pushCell('Balance:');
$cells->pushCell('$1000');
return TableRows::make([TableRow::make($cells), $default]);
};
}
Search
protected function search(): array
{
return ['id', 'title', 'text'];
}
Full-Text Search
use MoonShine\Support\Attributes\SearchUsingFullText;
#[SearchUsingFullText(['title', 'text'])]
protected function search(): array
{
return ['id'];
}
Relation Search
protected function search(): array
{
return ['category.title'];
}
See references/filters-search.md for filters, query tags, JSON search, global search, and query modification.
Filters
Filters use the same field classes displayed on the index page:
use MoonShine\UI\Fields\Text;
protected function filters(): iterable
{
return [
Text::make('Title', 'title'),
];
}
Cache filter state with:
protected bool $saveQueryState = true;
Modal CRUD
Open create, edit, or detail views in modal windows on the index page:
class PostResource extends ModelResource
{
protected bool $createInModal = true;
protected bool $editInModal = true;
protected bool $detailInModal = true;
}
Active Actions
Control which CRUD operations are available globally:
use MoonShine\Support\ListOf;
use MoonShine\Laravel\Enums\Action;
protected function activeActions(): ListOf
{
return parent::activeActions()
->except(Action::VIEW, Action::MASS_DELETE);
// or: ->only(Action::VIEW)
}
Available actions: Action::CREATE, Action::VIEW, Action::UPDATE, Action::DELETE, Action::MASS_DELETE.
Redirects
use MoonShine\Support\Enums\PageType;
// Via property (redirect to form after save)
protected ?PageType $redirectAfterSave = PageType::FORM;
// Via method (custom URL)
public function getRedirectAfterSave(): string
{
return '/';
}
public function getRedirectAfterDelete(): string
{
return $this->getIndexPageUrl();
}
Validation
protected function rules(mixed $item): array
{
return [
'title' => ['required', 'string', 'min:5'],
];
}
public function validationMessages(): array
{
return [
'email.required' => 'Email is required',
];
}
public function prepareForValidation(): void
{
request()?->merge([
'email' => request()?->string('email')->lower()->value(),
]);
}
// Enable precognitive validation
protected bool $isPrecognitive = true;
Events Lifecycle
Override these methods in your resource to hook into CRUD operations:
protected function beforeCreating(mixed $item): mixed { return $item; }
protected function afterCreated(mixed $item): mixed { return $item; }
protected function beforeUpdating(mixed $item): mixed { return $item; }
protected function afterUpdated(mixed $item): mixed { return $item; }
protected function beforeDeleting(mixed $item): mixed { return $item; }
protected function afterDeleted(mixed $item): mixed { return $item; }
protected function beforeMassDeleting(array $ids): void {}
protected function afterMassDeleted(array $ids): void {}
See references/events-buttons.md for event examples, button customization, import/export, and metrics.
Buttons Overview
use MoonShine\Support\ListOf;
use MoonShine\UI\Components\ActionButton;
// Add buttons above the index table (next to Create)
protected function topButtons(): ListOf
{
return parent::topButtons()->add(
ActionButton::make('Refresh', '#')
->dispatchEvent(AlpineJs::event(JsEvent::TABLE_UPDATED, $this->getListComponentName()))
);
}
// Per-row buttons in the index table (also supports ->bulk() for bulk actions)
protected function indexButtons(): ListOf
{
return parent::indexButtons()
->add(ActionButton::make('Link', '/endpoint'))
->add(ActionButton::make('Bulk Action', '/endpoint')->bulk());
}
Query Modification
use Illuminate\Contracts\Database\Eloquent\Builder;
// Modify all queries for this resource
protected function modifyQueryBuilder(Builder $builder): Builder
{
return $builder->where('active', true);
}
// Modify query for single-record retrieval
protected function modifyItemQueryBuilder(Builder $builder): Builder
{
return $builder->withTrashed();
}
Component Modifiers
Override the main component on any CRUD page from the resource:
use MoonShine\Contracts\UI\ComponentContract;
public function modifyListComponent(ComponentContract $component): ComponentContract
{
return parent::modifyListComponent($component)->customAttributes(['data-my-attr' => 'value']);
}
public function modifyFormComponent(ComponentContract $component): ComponentContract
{
return parent::modifyFormComponent($component)->customAttributes(['data-my-attr' => 'value']);
}
public function modifyDetailComponent(ComponentContract $component): ComponentContract
{
return parent::modifyDetailComponent($component)->customAttributes(['data-my-attr' => 'value']);
}
Page Components (Quick Add)
Add extra components to pages without publishing page classes:
use MoonShine\UI\Components\FormBuilder;
use MoonShine\UI\Components\Modal;
use MoonShine\UI\Fields\Text;
// Added to bottomLayer of all pages
protected function pageComponents(): array
{
return [
Modal::make('My Modal', components: [
FormBuilder::make()->fields([
Text::make('Title'),
])
])->name('demo-modal'),
];
}
// Or target specific pages:
// indexPageComponents(), formPageComponents(), detailPageComponents()
Resource Lifecycle Hooks
// Fires when resource is loaded and active
protected function onLoad(): void
{
// e.g., add assets, push components to layers
}
// Fires when MoonShine creates the resource instance
protected function onBoot(): void
{
// e.g., early initialization
}
Traits can hook in via load{TraitName} / boot{TraitName} naming convention.
Authorization
protected bool $withPolicy = true; // enable Laravel Policy checks
Available Policy methods: viewAny, view, create, update, delete, massDelete, restore, forceDelete.
Generate a policy:
php artisan moonshine:policy PostPolicy
Routes Helper
$resource->getUrl(); // first page
$resource->getIndexPageUrl(); // index page
$resource->getFormPageUrl(); // create page
$resource->getFormPageUrl(1); // edit page by ID
$resource->getDetailPageUrl($item); // detail page
$resource->getAsyncMethodUrl('method'); // custom async method
$resource->getRoute('crud.update', $id); // CRUD routes
Response Modifiers (Async Mode)
use MoonShine\Laravel\Http\Responses\MoonShineJsonResponse;
public function modifySaveResponse(MoonShineJsonResponse $response): MoonShineJsonResponse
{
return $response;
}
public function modifyDestroyResponse(MoonShineJsonResponse $response): MoonShineJsonResponse
{
return $response;
}
public function modifyMassDeleteResponse(MoonShineJsonResponse $response): MoonShineJsonResponse
{
return $response;
}
Cross-References
- For field types and relationship fields, see the
moonshine-fields-v3skill. - For ActionButton advanced usage, see the
moonshine-components-v3skill. - For Layout and menu configuration, see the
moonshine-appearance-v3skill. - For TableBuilder and FormBuilder components, see the
moonshine-components-v3skill. - Detailed CRUD page customization:
references/crud-pages.md - Filters, search, and query details:
references/filters-search.md - Events, buttons, import/export, metrics:
references/events-buttons.md