jeudi 25 janvier 2018

Laravel Model Inheritance through Polymorphic Relation

I was searching a way to implement Laravel Model Inheritance and I was inspired by the Multi Table Inheritance described at the accepted answer here: How can I implement single table inheritance using Laravel's Eloquent?

@lukasgeiter ends with: "So as you can see, a clean database comes with it's price. It will be a bit more complex to handle your model. However you can put all these extra logic (e.g. that's required to create a model) into a function in your model class and don't worry about it too much."

I did it using Laravel's Polymorphic Relations and an abstract subclass of the father class, from which to have all the children inherit, using PHP magic methods.

My concern is if by chance I stumbled into a sort of antipattern or bad practice, so I would like to hear some expert opinion about it.

I did not use the *able convention, since I'm implementing a father/child schema using morphOne, not a standard polymorphic Relation. Laravel version in 5.2, a little bit old, but right now I can't upgrade it.

The DB is composed of many salable parts, all of which are coded. All codes have common behaviors, while the parts differ from one another. Following a db sample:

code table:
id | code   | child_id | child_type | other_code_fields
1  | XXX123 | 1        | BrakePad   | ...
2  | YYY987 | 1        | BrakeShoe  | ...


brake_pads table:
id | length | width | other_brake_pads_fields
1  | 10     | 20    | ...


brake_shoes table:
id | material | other_brake_shoes_fields
1  | xyzacb   | ...

The Father class inherit directly from Model, implements the morphTo relation and the __get magic method in order to access children properties directly like $code->length, instead oh passing through $code->child->length.

<?php
class Code extends Model {

    protected $child_fields = [];

    public function child() {
        return $this->morphTo();
    }

    public function __get($name) {
        $ret = parent::__get($name);

        if ($ret === null) {

            if (isset($this->child_fields[$name])) {
                $ret = $this->child_fields[$name];
            } else if (array_key_exists('child', $this->getRelations())) {
                $ret = $this->child->{$name};
            }
        }

        return $ret;
    }

    public function __toString() {
        return $this->code;
    }

}

The Child abstract class is where the bulk logic is implemented, mostly by implemnting PHP magic methods and overriding Model::save() and Model::delete(). Thanks to this implementation it is possible to access the properties of the parent class (Code) directly from the child class (eg BrakePad) via the PHP Object Operator. For example it's possible to use $bp->code instead of $bp->father->code to access the 'code' property. The same technique allows both to read and write the properties, taking care to save the data in the appropriate tables.

<?php
abstract class CodeChild extends Code {

    protected $father_fields = [];

    public function father() {
        return $this->morphOne(Code::class, 'child');
    }

    public function __get($name) {
        $ret = parent::__get($name);

        if ($ret === null) {
            if (isset($this->father_fields[$name])) {
                $ret = $this->father_fields[$name];
            } else if ($this->father()->count() > 0) {
                $ret = $this->father->{$name};
            }
        }

        return $ret;
    }

    public function __set($key, $value) {

        if (!Schema::hasColumn($this->getTable(), $key)) {
            $this->father_fields[$key] = $value;
        } else {
            parent::__set($key, $value);
        }
    }

    public function __unset($key) {

        if (!Schema::hasColumn($this->getTable(), $key)) {
            unset($this->father_fields[$key]);
        } else {
            parent::__unset($key);
        }
    }

    public function __isset($key) {

        if (!Schema::hasColumn($this->getTable(), $key)) {
            return isset($this->father_fields[$key]);
        } else {
            return parent::__isset($key);
        }
    }

    public function save(array $options = array()) {

        $ret = parent::save($options);

        if ($ret) {

            $father = $this->father;
            if ($father == null) {
                $father = new \App\Code();
            }

            foreach ($this->father_fields as $key => $value) {
                $father->{$key} = $value;
            }

            $father->save();
            $this->father()->save($father);
        }

        return $ret;
    }

    public function delete() {
        $ret = false;
        if ($this->father->delete()) {
            $ret = parent::delete();
        }
        return $ret;
    }

}

Finally we have the "real" product class, with its own logic and all Code logic inherited:

<?php
class BrakePad extends CodeChild {
    /* Custom logic */
}

The inverse of the relationship, that is, access to the parameters of the child instances from an instance of the parent class, is available as read-only, as there could be a "Code" object unlinked from any children. It is therefore advisable, except in special cases, to use the CRUD functionalities of the ORM other than Read only on instances of the child classes.

Mine is not really a question, but a request for opinions about an advanced technique of data modeling about which I have found many questions on StackOverflow and on the internet, but few satisfactory answers. I hope I have not violated any rules.

Aucun commentaire:

Enregistrer un commentaire