Skip to content

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.

Either generate migration, model, entity and resource in one go:

Terminal window
php spark make:anvil-model Invoice --fields "number!:string:30,customer_id!:belongs_to,amount:decimal:12.2,due_on:date" --soft-deletes --resource
php spark migrate

and 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);
}
}
Terminal window
php spark migrate
Terminal window
php spark make:anvil-resource Invoice

This 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.

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.

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.

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.

  • Resources – labels, navigation, relations, actions, extension points
  • Forms – fields and validation
  • Schemas – sections, tabs and wizards
  • Tables – columns, filters, cards