Ryan Durham
Stage Right Labs
ryan at stagerightlabs dot com
"Represents an inheritance hierarchy of classes as a
single table that has columns for all the fields
of the various classes."
- Martin Fowler, Patterns of
Enterprise Application Architecture
Let's look at an example.
Imagine we are tracking medical records.
The status of the record request dictates the type of actions that can be taken against it.
<?php foreach($records as $record) ?>
<tr>
<td><?php echo $record->referenceNum; ?></td>
<td>
<?php if ($record->status == 'new') ?>
<a href="...">Cancel</a>
<a href="...">Assign to Nurse</a>
<?php endif; ?>
<?php if ($record->status == 'acquired') ?>
<a href="...">Prepare Summary</a>
<a href="...">View PDFs</a>
<?php endif; ?>
<?php if ($record->status == 'summarized') ?>
<a href="...">Review Summary</a>
<a href="...">Approve</a>
<a href="...">Reject</a>
<?php endif; ?>
<?php if ($record->status == 'approved') ?>
<a href="...">View</a>
<a href="...">Process Payment</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
This works, but the view code is very cluttered, and we are not even accounting for user access control.
Is there a better way?
Single Table Inheritance is an excellent alternative.
By implementing STI we can ask our entity objects to define their own allowable actions.
public function getActions($userLevel)
{
return $this->actions[$userLevel];
}
protected $actions = [
'client' => [
[
'name' => 'edit',
'action' => 'ClientReportController@edit',
'class' => 'default'
],
[
'name' => 'delete',
'action' => 'ClientReportController@delete',
'class' => 'danger'
],
]
// Additional User level entries ...
];
<?php foreach($records as $record) ?>
<tr>
<td>
<?php echo $record->referenceNum; ?>
</td>
<td>
<?php echo $record->getActions('client'); ?>
</td>
</tr>
<?php endforeach; ?>
Discriminator Column: The table column used to determine the entity type.
Inheritance Map: A way to associate discrimination values with php classes.
// app/Epiphyte/Record.php
class Record extends Illuminate\Database\Eloquent\Model {
// Eloquent Configuration
protected $guarded = ['id'];
protected $fillable = ['name', 'description', 'status'];
// Single Table Inheritance Configuration
use SingleTableInheritanceTrait;
protected $table = 'records';
protected $morphClass = 'Epiphyte\Record';
protected $discriminatorColumn = 'status';
protected $inheritanceMap = [
'new' => 'Epiphyte\Entities\Records\RecordNew',
'requested' => 'Epiphyte\Entities\Records\RecordRequested',
'retrieved' => 'Epiphyte\Entities\Records\RecordRetrieved',
'complete' => 'Epiphyte\Entities\Records\RecordComplete'
];
// ...
}
// app/Epiphyte/Entities/Records/RecordNew.php
class RecordNew extends Epiphyte\Record {
/**
* Limit the query scope if we define a query against
* the base table using this class.
*/
public function newQuery($excludeDeleted = true)
{
return parent::newQuery($excludeDeleted)
->where('status', '=', 'new');
}
// Remaining child entity methods go here...
}
// Illuminate\Database\Eloquent\Model
public function mapData(array $attributes)
{
// Determine the entity type
$entityType = isset($attributes[$this->discriminatorColumn]) ?
$attributes[$this->discriminatorColumn] : null;
// Throw an exception if this entity type
// is not in the inheritance map
if (!array_key_exists($entityType, $this->inheritanceMap)) {
throw new ModelNotFoundException();
}
// Get the appropriate class name from
// the inheritance map
$class = $this->inheritanceMap[$entityType];
// Return a new instance of the specified class
return new $class;
}
// Illuminate\Database\Eloquent\Model
public function newFromBuilder($attributes = array())
{
// Create a new instance of the Entity Type Class
$m = $this->mapData((array)$attributes)
->newInstance(array(), true);
// Hydrate the new instance with the table data
$m->setRawAttributes((array)$attributes, true);
// Return the assembled object
return $m;
}
RecordNew::all();
The Data Mapper pattern lends itself more easily to Single Table Inheritance
/**
* @Entity
* @InheritanceType("SINGLE_TABLE")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"person" = "Person", "employee" = "Employee"})
*/
class Person
{
// ...
}
/**
* @Entity
*/
class Employee extends Person
{
// ...
}
STI is ideal for situations where the child entities all make use of the same table column structure.
Good:
Inheriting Ford
, Toyota
and Mercedes
types from an automobiles
table.
Bad:
Inheriting Car
, Motorcyle
and Airplane
types from a vehicles
table.
Adding table columns that will only be used by specific child entities is a warning sign.
STI may not be a good choice in that case.
Questions?
ryan at stagerightlabs dot com