Eloquent: the Masquerade
I have to implement a Laravel application where many different types of "service" are handled. Different services may have different data. Different services have mostly all the same functionalities: have statuses, trigger notifications, and more in general are related to the same types of other entities (users, clients, payments, documents...).
So, I'm trying a particular approach: there is a single services
table, and a single Service
Eloquent Model, but many other classes extending that same Service
, one for each type. Each extending class has his own parameters, to describe his specific behavior (his possible statuses, the type of payments it refers, and more). In this way I have to implement all surrounding code always expecting a Service
instance, while each instance has his own configuration and eventually his own methods to be invoked in specific situations (e.g. in the Controller dedicated to each different service).
The master Service
model looks like:
class Service extends Model
{
/*
Each trait implies some abstract method to be implemented by each child
class, to define his own behavior, and includes the proper Eloquent
relations
*/
use HasDocuments, HasPayments, HasCommunications, HasStatus, HasDates, Linkable;
protected $table = 'services';
protected function casts(): array
{
return [
/*
In a generic JSON object are stored service-specific informations
*/
'data' => 'object',
];
}
/*
This will transform each instance fetched from the database in an instance
of the proper child model
*/
public function newFromBuilder($attributes = [], $connection = null)
{
$type = $attributes->type ?? null;
$model = null;
if ($type) {
$types = app()->make('reflection')->allServiceTypes();
if (isset($types[$type])) {
$class = $types[$type];
$model = (new $class)->newInstance([], true);
}
}
if ($model === null) {
$model = $this->newInstance([], true);
}
$model->setRawAttributes((array) $attributes, true);
$model->setConnection($connection ?: $this->getConnectionName());
$model->fireModelEvent('retrieved', false);
return $model;
}
/*
Enforcement to correctly wire polymorphic relations over the child classes
*/
public function getMorphClass()
{
return self::class;
}
}
Each Service
row in the database has a type
attribute: a simple string statically defined by each child class:
class ATypeService extends Service
{
public static $type = 'a_type_of_service';
protected $service;
public function __construct($service = null)
{
parent::__construct();
$this->service = $service;
}
/*
All other stuff...
*/
}
The "reflection" singleton mentioned in the code above helps in mapping this type
string with the proper class:
class Reflection
{
private $types = null;
public function allServices()
{
return [
\App\Services\ATypeService::class,
\App\Services\AnotherService::class,
\App\Services\YetDifferentThing::class,
];
}
public function allServiceTypes()
{
if ($this->types == null) {
$this->types = [];
foreach($this->allServices() as $class) {
$this->types[$class::$type] = $class;
}
}
return $this->types;
}
}
This approach is a bit radical, but permits a very high level of abstraction: as everything is a Service
, in most methods and function (storing, updating, displaying the status of related payments or the list of attached documents...) I don't have to really care about the actual type of service.