Search code examples
phpvalidationyii2modelscomposite

Yii2 // Model as Model attribute


actually im working on a project, where i want to have all DB-tables as Models. But now im stucking at one Point. Lets say i have a "Master"-Table where many different relations are defined like the following (easy example):

Human has one heart; Human has one brain... and so on... Is it possible, to fill up the Master-Model with other Models? In PHP it would looks like that:

$human = new Human();
$human->heart = new Heart(); 
$human->brain = new Brain(); 

Finally i want to say:

$human-save(TRUE);

to VALIDATE all relational models AND save all relational data and the human object in DB.

Is that possible? I cant find something like that on the whole internet O_o.

Thank you very much!


Solution

  • I suggest you following approach:

    1. Let's say you have same relation names as property names for nested objects (some rule needed to call $model->link() method)
    2. Declare common class for Models with nested Models (for example ActiveRecordWithNestedModels)
    3. Override in common class methods save and validate to perform cascade for these operations (using reflection)
    4. Let your models will inherit this common class

    Or, as an alternative for overriding validate method, you can build some suitable implementation for rules method in common class.

    This common class can looks as follows (this is a simple draft, not tested, just to show the conception):

    <?php
    
    namespace app\models;
    
    use yii\db\ActiveRecord;
    
    class ActiveRecordWithNestedModels extends ActiveRecord
    {
    
        public function save($runValidation = true, $attributeNames = null)
        {
            $saveResult = parent::save($runValidation, $attributeNames);
    
            $class = new \ReflectionClass($this);
    
            foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
                $propertyValue = $property->getValue($this);
                if (!empty($propertyValue) && is_subclass_of($propertyValue, ActiveRecord::className())) {
                    /* @var ActiveRecord $nestedModel */
                    $nestedModel = $propertyValue;
                    $nestedModel->save($runValidation);
                    $relation = $property->name;
                    $this->link($relation, $nestedModel);
                }
            }
    
            return $saveResult;
        }
    
        public function validate($attributeNames = null, $clearErrors = true)
        {
            $class = new \ReflectionClass($this);
    
            foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
                $propertyValue = $property->getValue($this);
                if (!empty($propertyValue) && is_subclass_of($propertyValue, ActiveRecord::className())) {
                    /* @var ActiveRecord $nestedModel */
                    $nestedModel = $propertyValue;
    
                    if (!$nestedModel->validate(null, $clearErrors)) {
                        array_push($this->errors, [
                            $property->name => $nestedModel->errors
                        ]);
                    }
                }
            }
    
            parent::validate($attributeNames, $clearErrors);
    
            if ($this->hasErrors()) return false;
    
            return true;
        }
    
    }
    

    Then your models can looks like this:

    class Heart extends ActiveRecordWithNestedModels
    {
    
    }
    
    class Human extends ActiveRecordWithNestedModels
    {
        /* @var Heart $heart */
        public $heart = null;
    
        /**
         * The relation name will be 'heart', same as property `heart'
         *
         * @return \yii\db\ActiveQuery
         */
        public function getHeart()
        {
            return $this->hasOne(Heart::className(), ['id', 'heart_id']);
        }
    }
    

    And (in theory) you can do:

    $human = new Human();
    $human->heart = new Heart();
    $human->save();
    

    P.S. here can be many complex details in further implementation, as for example

    • using transactions to rollback save if some child object fails save
    • overriding delete
    • serving one-to-many and many-to-many relations
    • skip cascade if property has no corresponding relation
    • serving $attributeNames in cascade operations
    • etc