Skip to content

Your first Anvil model

Anvil models are regular CodeIgniter models with additional, enforced rules: public UUIDs, tenant isolation and auditing. This page creates a Project model. The rules themselves are explained in Data rules.

Terminal window
php spark make:anvil-model Project --fields "name!:string:150,budget:decimal:12.2,starts_on:date" --soft-deletes
php spark migrate

This writes the migration, the model and the entity described below. name! marks a required column; the types are string[:length], text, int, decimal[:12.2], bool, date, datetime, json and belongs_to[:table]. Add --resource to also generate the resource, --no-tenant for a table shared by all tenants. All options: Commands.

The rest of this page shows the same files written by hand, so you know what to keep when you adjust them.

Every Anvil table needs a uuid column of type BINARY(16) with a unique key. Tenant scoped tables also need a tenant_id column.

<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateProjects 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],
'name' => ['type' => 'VARCHAR', 'constraint' => 150],
'budget' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => true],
'starts_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->addForeignKey('tenant_id', 'anvil_tenants', 'id', 'CASCADE', 'CASCADE');
$this->forge->createTable('projects', false, ['ENGINE' => 'InnoDB']);
}
public function down(): void
{
$this->forge->dropTable('projects', true);
}
}
<?php
namespace App\Entities;
use Anvil\Data\Entity;
class Project extends Entity
{
}

The entity leaves internal fields (id, tenant_id) out of toPublicArray() and JSON. List further internal columns in protected array $internal = [...]. Models may also return arrays ($returnType = 'array'); Anvil works with both.

<?php
namespace App\Models;
use Anvil\Data\Model;
use App\Entities\Project;
class ProjectModel extends Model
{
protected $table = 'projects';
protected $returnType = Project::class;
protected $useSoftDeletes = true;
protected $useTimestamps = true;
protected $allowedFields = ['name', 'budget', 'starts_on'];
}

Do not add uuid or tenant_id to $allowedFields. Anvil sets them and ignores changes to them. The uuid cast is registered automatically.

service('tenantContext')->set($tenantId); // in panels the tenant filter does this
$projects = model(ProjectModel::class);
$projects->insert(['name' => 'Website relaunch']);
$uuid = $projects->getInsertUuid(); // Anvil\Data\Uuid
$project = $projects->findByUuid($uuid); // only finds records of the current tenant
echo json_encode($project); // {"uuid":"0192...","name":"Website relaunch",...}

Without a tenant in the context, the model throws a TenancyException instead of reading or writing data of all tenants.

The insert has also written an entry to the audit log, in the same transaction:

event subject_type changes
record.created projects {"name":[null,"Website relaunch"]}

Create a resource for the model, so users can list and edit projects in the panel.