Your first resource
A resource describes how the records of one model are listed, shown, created and edited. This page builds a resource for invoices.
1. Create the table
Section titled “1. Create the table”Either generate migration, model, entity and resource in one go:
php spark make:anvil-model Invoice --fields "number!:string:30,customer_id!:belongs_to,amount:decimal:12.2,due_on:date" --soft-deletes --resourcephp spark migrateand continue with step 3, or write the migration yourself:
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateInvoices extends Migration{ public function up(): void { $this->forge->addField([ 'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], 'uuid' => ['type' => 'BINARY', 'constraint' => 16], 'tenant_id' => ['type' => 'INT', 'unsigned' => true], 'customer_id' => ['type' => 'INT', 'unsigned' => true], 'number' => ['type' => 'VARCHAR', 'constraint' => 30], 'status' => ['type' => 'ENUM', 'constraint' => ['draft', 'sent', 'paid'], 'default' => 'draft'], 'amount' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'default' => '0.00'], 'due_on' => ['type' => 'DATE', 'null' => true], 'created_at' => ['type' => 'DATETIME', 'null' => true], 'updated_at' => ['type' => 'DATETIME', 'null' => true], 'deleted_at' => ['type' => 'DATETIME', 'null' => true], ]); $this->forge->addPrimaryKey('id'); $this->forge->addUniqueKey('uuid'); $this->forge->addKey('tenant_id'); $this->forge->addKey('customer_id'); $this->forge->addForeignKey('tenant_id', 'anvil_tenants', 'id', 'CASCADE', 'CASCADE'); $this->forge->addForeignKey('customer_id', 'customers', 'id', 'CASCADE', 'RESTRICT'); $this->forge->createTable('invoices', false, ['ENGINE' => 'InnoDB']); }
public function down(): void { $this->forge->dropTable('invoices', true); }}php spark migrate2. Generate the resource
Section titled “2. Generate the resource”php spark make:anvil-resource InvoiceThis reads the columns of invoices and creates:
| File | Content |
|---|---|
app/Resources/InvoiceResource.php |
form fields and list columns derived from the columns |
app/Models/InvoiceModel.php |
Anvil model with $allowedFields |
app/Entities/Invoice.php |
entity with boolean casts |
Existing files are never overwritten unless you pass --force. Use
--table when the table name is not the plural of the resource name. The
table must have a uuid column, otherwise the command stops.
3. Read and adjust the code
Section titled “3. Read and adjust the code”The generated code is a starting point, meant to be read and adjusted, not regenerated. The fields are derived from the column types:
| Column | Field | List column |
|---|---|---|
ENUM |
Select with the values |
badge |
TINYINT(1), or TINYINT named is_*, has_*, active, … |
Toggle |
yes / no |
integer *_id |
searchable BelongsTo on <Name>Model (customer_id → CustomerModel) |
title of the related record |
DECIMAL named like an amount (amount, price, total, budget, …) |
NumberInput::currency('EUR') |
money |
other DECIMAL, integers |
NumberInput with the decimals of the column, integer() |
number |
DATE, DATETIME |
DateInput, withTime() for date and time |
date |
TEXT |
Textarea |
shortened text |
JSON |
TagsInput for names containing tag, otherwise a TODO comment |
– |
| strings | TextInput with the length of the column, email(), tel() or url() by name |
searchable text |
NOT NULL columns without a default become required(). The list shows
the first eight columns; from the fifth on they are toggleable.
A typical result after a few changes:
final class InvoiceResource extends Resource{ protected string $model = InvoiceModel::class; protected string $recordTitleAttribute = 'number'; protected string $navigationIcon = 'receipt'; protected ?string $navigationGroup = 'Sales';
public function form(Form $form): Form { return $form->schema([ Section::make('Invoice')->columns(2)->schema([ TextInput::make('number')->required()->maxLength(30), BelongsTo::make('customer_id')->label('Customer')->relationship(CustomerModel::class, 'name')->searchable()->required(), Select::make('status')->options(['draft' => 'Draft', 'sent' => 'Sent', 'paid' => 'Paid'])->required()->default('draft'), NumberInput::make('amount')->currency('EUR')->min(0), DateInput::make('due_on'), ]), ]); }
public function collection(Collection $collection): Collection { return $collection ->columns([ TextColumn::make('number')->sortable()->searchable(), TextColumn::make('customer_id')->label('Customer')->relationship(CustomerModel::class, 'name'), TextColumn::make('status')->badge()->sortable(), TextColumn::make('amount')->money('EUR')->sortable(), TextColumn::make('due_on')->date()->sortable(), ]) ->defaultSort('number'); }}Forms: Forms, Schemas. Lists: Tables.
4. Register it in the panel
Section titled “4. Register it in the panel”final class AdminPanel extends Panel{ public function resources(): array { return [ InvoiceResource::class, ...Access::resources(), ]; }}The panel now has the routes /admin/invoices, /admin/invoices/create,
/admin/invoices/{uuid} and /admin/invoices/{uuid}/edit, and a navigation
entry.
5. Grant the permissions
Section titled “5. Grant the permissions”The resource defines the permissions invoices.view, invoices.create,
invoices.update, invoices.delete and invoices.history. The
Administrator role created by anvil:install has full access; grant the
permissions to other roles under Access → Roles. Users without
invoices.view do not see the navigation entry and get 403 when they
open the URL.