Eloquent: the Masquerade

Eloquent: the Masquerade
Photo by Llanydd Lloyd / Unsplash

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.