nanigans/single-table-inheritance
Installation:
composer require nanigans/single-table-inheritance
Add the SingleTableInheritance trait to your base model:
use Nanigans\SingleTableInheritance\SingleTableInheritance;
class BaseModel extends Model
{
use SingleTableInheritance;
}
Define Inheritance Hierarchy: Extend your base model with child models:
class ChildModel extends BaseModel { }
class AnotherChildModel extends BaseModel { }
Configure Discriminator Column (optional):
Override getDiscriminatorColumn() in your base model:
protected function getDiscriminatorColumn(): string
{
return 'model_type';
}
First Use Case:
// Create a child model
$child = new ChildModel();
$child->save(); // Automatically sets `model_type` to 'App\ChildModel'
// Retrieve by type
$child = BaseModel::where('model_type', 'App\ChildModel')->first();
Model Creation & Persistence:
The trait automatically populates the discriminator column (model_type by default) with the fully qualified class name. No manual intervention required.
Polymorphic Queries:
Use ofType() to filter by model type:
$childModels = BaseModel::ofType(ChildModel::class)->get();
Dynamic Casting: Retrieve and cast models to their concrete types:
$baseModel = BaseModel::find(1);
$concreteModel = $baseModel->cast(); // Returns ChildModel instance
Integration with Other Traits:
Works seamlessly with Laravel’s built-in traits (e.g., SoftDeletes, Timestamps) and third-party traits like Validating or Observables.
Custom Discriminator Values:
Override getDiscriminatorValue() to use non-FQCN values (e.g., short class names):
protected function getDiscriminatorValue(): string
{
return class_basename(static::class);
}
Deep Inheritance Hierarchies:
Supports multi-level inheritance (e.g., BaseModel → IntermediateModel → ChildModel). The trait resolves the correct type automatically.
API Responses:
Use getDiscriminatorValue() in API responses to expose the model type:
return response()->json([
'type' => $model->getDiscriminatorValue(),
'data' => $model->attributes,
]);
Event Handling: Dispatch polymorphic events by type:
$model->cast()->dispatchEvent(); // Triggers events for the concrete type
Discriminator Column Mismatch:
Ensure the discriminator column exists in your database table. The trait defaults to model_type but can be customized.
Case Sensitivity:
The discriminator value is case-sensitive by default. Use strtolower() in getDiscriminatorValue() if case insensitivity is needed:
protected function getDiscriminatorValue(): string
{
return strtolower(class_basename(static::class));
}
Circular Dependencies:
Avoid circular inheritance (e.g., A extends B and B extends A). The trait assumes a valid hierarchy.
Mass Assignment:
The discriminator column is not mass assignable by default. Explicitly add it to $fillable if needed:
protected $fillable = ['model_type', 'other_field'];
Verify Discriminator Values: Check the stored value in the database matches the expected FQCN or custom value:
$model->getDiscriminatorValue(); // Debug the actual value
Cast Failures:
If $model->cast() returns null, ensure:
Query Performance:
Avoid ofType() with dynamic class names (e.g., $class = $request->input('type'); BaseModel::ofType($class)). This can lead to SQL injection or N+1 queries. Use a whitelist or whereIn():
$allowedTypes = [ChildModel::class, AnotherChildModel::class];
$models = BaseModel::whereIn('model_type', $allowedTypes)->get();
Custom Discriminator Logic:
Override getDiscriminatorColumn() or getDiscriminatorValue() for custom behavior.
Hybrid Inheritance:
Combine with Concrete Table Inheritance by adding a table column and overriding getTable():
protected function getTable(): string
{
return $this->table ?? 'shared_table';
}
Serialization: Customize JSON serialization to include/exclude the discriminator:
public function toArray()
{
return array_merge($this->attributes, [
'type' => $this->getDiscriminatorValue(),
]);
}
Testing:
Mock the trait in unit tests by overriding cast() or getDiscriminatorValue():
$model = new BaseModel();
$model->shouldReceive('getDiscriminatorValue')->andReturn('MockType');
How can I help you explore Laravel packages today?