A Laravel package for implementing the Class Table Inheritance (CTI) pattern with Eloquent models. Shared columns live in one parent table, type-specific columns live in their own tables, and a foreign key ties them together. The package handles type resolution, querying, and persistence automatically.
illuminate/database >=8.0 <14.0)composer require pannella/laravel-cti
CTI uses three layers of tables: an optional lookup table for type definitions, a parent table for shared columns, and one or more subtype tables for type-specific columns.
// Parent table: shared attributes
Schema::create('assessments', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->foreignId('type_id')->constrained('assessment_types');
$table->timestamps();
});
// Subtype table: quiz-specific attributes
Schema::create('assessment_quiz', function (Blueprint $table) {
$table->unsignedBigInteger('assessment_id')->primary();
$table->integer('passing_score')->nullable();
$table->integer('time_limit')->nullable();
$table->boolean('show_correct_answers')->default(false);
$table->foreign('assessment_id')->references('id')->on('assessments')->onDelete('cascade');
});
// Parent model
class Assessment extends Model
{
use HasSubtypes;
protected static $subtypeMap = [
'quiz' => Quiz::class,
'survey' => Survey::class,
];
protected static $subtypeKey = 'type_id';
protected static $subtypeLookupTable = 'assessment_types';
protected static $subtypeLookupKey = 'id';
protected static $subtypeLookupLabel = 'label';
protected $fillable = ['title', 'type_id'];
}
// Subtype model
class Quiz extends SubtypeModel
{
protected $table = 'assessments'; // must be the parent table
protected $subtypeTable = 'assessment_quiz';
protected $subtypeAttributes = ['passing_score', 'time_limit', 'show_correct_answers'];
protected $ctiParentClass = Assessment::class;
protected $fillable = ['passing_score', 'time_limit', 'show_correct_answers'];
}
// Create
$quiz = Quiz::create([
'title' => 'Final Exam',
'passing_score' => 80,
'time_limit' => 60,
]);
// Query (auto-joins subtype table when needed)
$hard = Quiz::where('passing_score', '>', 90)->get();
// Parent queries return correctly-typed subtype instances
$all = Assessment::all(); // mixed collection of Quiz, Survey, etc.
For the full setup guide including direct discriminator mode, PHP 8.1 attributes, and fillable/casts inheritance, see the Getting Started guide.
If you have a type hierarchy in Laravel (for example, Quiz and Survey are both types of Assessment), there are a few common ways to model it. Each has tradeoffs.
One table holds every column for every type, with a discriminator column to distinguish them.
Each type gets its own table (quizzes, surveys) with shared columns duplicated in each one.
Laravel's morphTo/morphMany pattern stores a *_type and *_id pair so one entity can relate to multiple unrelated model types.
Comment that can belong to either a Post or a Video).*_id column references different tables depending on the type value, you can't put a real foreign key constraint on it. Referential integrity is only enforceable in application code. Trying to use polymorphic relations to model a type hierarchy requires a lot of custom wiring and you lose the ability to query the parent type as a unified collection.Shared attributes live in a parent table, type-specific attributes live in separate subtype tables linked by foreign key.
CTI is the right fit when your subtypes share an identity (a Quiz is an Assessment), share common attributes (title, timestamps), and each type also has its own attributes (passing_score, anonymous). If your types are unrelated entities that just happen to share a relationship, polymorphic relations are the better tool.
MIT
How can I help you explore Laravel packages today?