Polymorphic, More or Less
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.