Polymorphic, More or Less

Polymorphic, More or Less
Photo by Joshua J. Cotten / Unsplash

An existing Laravel codebase, quite good but with a low level of abstraction, an a feature already existing for a given context (dates of availability for X) to replicate to another context (dates of availability for Y).

At first, a polymorphic relationship would have been the solution to abstract the connection between "availabilty" and "everything else", but I didn't want to refactor a whole codebase and fix all the occurrences (there are a few hard wired SQL queries, here and there...).

At the same time, I didn't want to copy&paste all the existing relevant code, change a few names, and deliver that. Fast and rough is OK, but there are limits...

So I introduced a new class, Availability, and coded the polymorphic-ish relation directly into it:

class Availability extends Model {
    protected $target = 'xxx';
    protected $table = 'xxx_availability';
    protected $primaryKey = 'xxx_availability_id';
    public $timestamps = true;

    public static function target($target)
    {
        $warp = new self();
        $warp->setTarget($target);
        $ret = self::query();
        $ret->setModel($warp);
        return $ret;
    }

    public function scopeTargetId($query, $id)
    {
        $key = $this->targetKey();
        $query->where($key, $id);
    }

    public function wire($obj)
    {
        $target = $this->setTargetFromObj($obj);
        $key = $this->targetKey();
        $this->$key = $obj->id();
    }

    public static function identifierFromClass($class)
    {
        return strtolower(Str::singular(class_basename($class)));
    }

    private function setTarget($target)
    {
        $this->target = $target;
        $this->table = sprintf('%s_availability', Str::plural($target));
        $this->primaryKey = sprintf('%s_availability_id', $target);
    }

    private function setTargetFromObj($obj)
    {
        $target = self::identifierFromClass(get_class($obj));
        $this->setTarget($target);
    }

    private function targetKey()
    {
        return sprintf('%s_id', $this->target);
    }
}

Now, each class having "availabilities" have his own table on the database: Foo has foos_availability, Bar has bars_availability and so on. When I want to retrieve availabilities for a given object, I can manually specify the group (yes, using the class would have been better: the static identifierFromClass method is intended for this...)

Availability::target('foo')->where(...)->get();

and target a specific object (yes, transparently extract the group from the object class would have been better...)

Availability::target('foo')->target_id($foo->id)->where(...)->get();

and create a new availability from the given object (this is fine!)

$a = new Availability();
$a->wire($foo);
$a->param1 = 'value1';
$a->param2 = 'value2';
$a->save();

and I have been able to write all related code once, in a trait for the controllers, to fullfit both needs "dates of availability for X" and "dates of availability for Y".

The code above is specific for my own use case, and for sure may vary on different situations, but the core concept are the lines

$warp = new self();
$warp->setTarget($target);
$ret = self::query();
$ret->setModel($warp);

A new Eloquent Builder is instantiated and a manipulated instance of the Availability class is passed as reference model: from here, it will write the SQL queries using the table and the keys generated in setTarget for the relevant name.