Installation:
composer require mannikj/laravel-sti
No additional configuration is required beyond publishing the trait.
Migration:
Add the sti() macro to your table migration:
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->sti()->nullable(); // Adds 'type' column with nullable constraint
});
First Use Case: Define a root model with the trait and child models:
use MannikJ\Laravel\SingleTableInheritance\Traits\SingleTableInheritance;
class Item extends Model {
use SingleTableInheritance;
}
class Book extends Item {}
class Movie extends Item {}
Now, when querying Item::all(), instances will automatically resolve to Book or Movie based on the type column.
Polymorphic Queries:
Use whereType() to filter by child class:
$books = Item::whereType(Book::class)->get(); // Returns only Book instances
Dynamic Model Resolution:
The trait handles instantiation automatically. No need for manual resolveClass() calls.
Scopes for Child Classes: Add class-specific scopes to child models:
class Book extends Item {
public function scopePublished($query) {
return $query->where('published_at', '<=', now());
}
}
Usage:
$publishedBooks = Book::published()->get();
Mass Assignment:
Ensure type is not mass-assignable unless explicitly allowed:
protected $guarded = ['type'];
API Responses:
Use getClass() to dynamically determine response formatting:
return response()->json([
'type' => $item->getClass(),
'data' => $item->toArray(),
]);
Validation: Validate child-specific fields in child models:
class Book extends Item {
public function rules() {
return [
'isbn' => 'required|string',
];
}
}
Relationships: Define polymorphic relationships where needed:
class User extends Model {
public function items() {
return $this->morphToMany([Book::class, Movie::class], 'itemable');
}
}
Missing type Column:
If the type column is missing or misnamed, queries will fail silently or return incorrect model types. Always verify migrations.
Case Sensitivity:
The type column stores fully qualified class names (e.g., App\Models\Book). Ensure your database collation handles case sensitivity if using custom class names.
Circular Dependencies:
Avoid circular inheritance (e.g., A extends B extends A). Laravel’s STI does not enforce this, but it will cause runtime errors.
Serialization:
Serialized models (e.g., cached data) may not resolve correctly if class names change. Use getClass() to reconstruct instances safely.
Incorrect Model Resolution:
Check the type column value in the database. It must match the fully qualified class name (e.g., App\Models\Book).
SELECT type FROM items WHERE id = 1;
Query Logs: Enable query logging to verify STI behavior:
DB::enableQueryLog();
$items = Item::all();
dd(DB::getQueryLog());
Custom Type Column:
Override the getStiType() method in the root model:
class Item extends Model {
use SingleTableInheritance;
public function getStiType(): string {
return 'custom_type_column';
}
}
Dynamic Class Resolution: Extend the trait to add custom logic:
class Item extends Model {
use SingleTableInheritance;
protected static function resolveClass($value) {
return match ($value) {
'legacy_book' => LegacyBook::class,
default => parent::resolveClass($value),
};
}
}
Soft Deletes:
Ensure child models also use SoftDeletes if the root model does:
use Illuminate\Database\Eloquent\SoftDeletes;
class Book extends Item {
use SoftDeletes;
}
Index the type Column:
Add an index for large tables to speed up whereType() queries:
Schema::table('items', function (Blueprint $table) {
$table->index('type');
});
Avoid N+1 Queries:
Use with() or load() for relationships to prevent eager-loading issues:
$books = Book::with('author')->get();
How can I help you explore Laravel packages today?