composer require pannella/laravel-cti
Schema::create('assessments', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('type')->comment('Discriminator column');
$table->timestamps();
});
Schema::create('assessment_quiz', function (Blueprint $table) {
$table->foreignId('assessment_id')->constrained()->onDelete('cascade');
$table->integer('passing_score');
$table->timestamps();
});
app/Models/Assessment.php):
use Pannella\LaravelCti\HasSubtypes;
class Assessment extends Model
{
use HasSubtypes;
protected static $subtypeMap = [
'quiz' => Quiz::class,
'survey' => Survey::class,
];
protected static $subtypeKey = 'type'; // Discriminator column
}
app/Models/Quiz.php):
use Pannella\LaravelCti\SubtypeModel;
class Quiz extends SubtypeModel
{
protected $subtypeTable = 'assessment_quiz';
protected $subtypeAttributes = ['passing_score'];
protected $ctiParentClass = Assessment::class;
}
// Create a Quiz (automatically handles parent + subtype tables)
$quiz = Quiz::create([
'title' => 'Math Test',
'passing_score' => 75,
]);
// Query subtypes directly (auto-joins parent table)
$hardQuizzes = Quiz::where('passing_score', '>', 90)->get();
// Query parent and get typed results
$allAssessments = Assessment::all(); // Returns mixed collection of Quiz/Survey
Key Files to Reference:
Creation:
// Subtype creation (parent + subtype tables)
$survey = Survey::create([
'title' => 'Customer Feedback',
'anonymous' => true,
]);
Reading:
// Parent query returns typed instances
$assessment = Assessment::find(1); // Returns Quiz or Survey
// Subtype query (auto-joins parent)
$quizzes = Quiz::whereHas('parent', fn($q) => $q->where('title', 'like', '%Exam%'))->get();
Updating:
// Update parent + subtype in one operation
$quiz->update(['title' => 'Updated Quiz', 'passing_score' => 80]);
Deletion:
$quiz->delete(); // Cascades to subtype table
Relationships:
// Parent has many subtypes
class Assessment extends Model
{
public function quizzes() { return $this->hasOne(Quiz::class); }
}
// Subtype belongs to parent
class Quiz extends SubtypeModel
{
public function parent() { return $this->belongsTo(Assessment::class); }
}
hasOne/belongsTo with subtype models (package handles the join logic).Events:
// Listen for subtype-specific events
Quiz::created(fn($quiz) => Log::info("Quiz created: {$quiz->title}"));
created, updated, deleted, saved, retrieved.Polymorphic Relations:
// Store assessments in a polymorphic relation
class User extends Model
{
public function assessments()
{
return $this->morphMany(Assessment::class, 'assessable');
}
}
API Resources:
class AssessmentResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'type' => $this->getSubtypeLabel(),
'data' => $this->getSubtypeAttributes(), // Returns quiz/survey-specific fields
];
}
}
getSubtypeLabel() and getSubtypeAttributes() for dynamic responses.Direct Discriminator Mode (no lookup table):
// Parent model
protected static $subtypeKey = 'type'; // Uses column directly
protected static $subtypeMap = ['quiz' => Quiz::class];
Subtypes Without Tables (for simple extensions):
class SimpleSurvey extends SubtypeModel
{
protected $subtypeTable = null; // No separate table
protected $subtypeAttributes = ['anonymous'];
}
Custom Type Resolution:
// Override type resolution logic
public function resolveSubtype()
{
return $this->type === 'premium' ? PremiumQuiz::class : Quiz::class;
}
Foreign Key Mismatches:
assessment_id in subtype table references id in parent table.php artisan schema:dump to verify constraints.Missing Subtype Handling:
null for unknown subtypes.config/cti.php:
'missing_subtype' => 'ignore', // or 'throw', 'null'
N+1 Queries:
withSubtypes():
$assessments = Assessment::withSubtypes()->get();
Fillable/Cast Inheritance:
$fillable in both parent and subtype:
// Parent
protected $fillable = ['title', 'type'];
// Subtype
protected $fillable = ['passing_score']; // Parent fillable still works
Timestamps:
timestamps = false in Blueprint.Query Logging: Enable Laravel’s query logging to inspect auto-generated joins:
DB::enableQueryLog();
$assessment = Assessment::find(1);
dd(DB::getQueryLog());
Type Resolution: Debug subtype resolution with:
$assessment = Assessment::find(1);
dd($assessment->getSubtypeClass(), $assessment->getSubtypeInstance());
Event Debugging: Listen for all events to trace the flow:
Assessment::created(fn($model) => Log::debug("Created: {$model->id}"));
Quiz::saved(fn($model) => Log::debug("Saved Quiz: {$model->id}"));
Batch Loading:
'batch_size' => 500, // For large datasets
Indexing:
Schema::table('assessment_quiz', function (Blueprint $table) {
$table->index('passing_score');
});
Caching:
php artisan cache:clear
resolveSubtype() in parentHow can I help you explore Laravel packages today?