Snippet/tip: Add/remove MANY_MANY & HAS_MANY relations automatically

While working on my first Yii project I found it troublesome to have code to manage HAS_MANY and MANY_MANY relations in every model. So I worte this function in order to handle this for me:




    protected function beforeSave() {

        return $this->loopRelations("beforeSave");

    }

    protected function afterSave() {

        return $this->loopRelations("afterSave");

    }

    protected function loopRelations($action) { 

        $action = (!empty($action) || ($action == "beforeSave" || $action == "afterSave")) ? $action == "beforeSave" : null;

        $state = TRUE;

        if ($action !== null) {

            $relations = $this->relations();

            $myPk = $this->primaryKey;

            if (count($relations) > 0) {

                foreach ($relations as $relation) {

                    list($type, $modelName, $foreignKey) = $relation;

                    $propertyPrefix = $action ? "remove" : "add";

                    $propertyName = $propertyPrefix . ucfirst($modelName[0]) . substr($modelName, 1);

                    if (property_exists($this, $propertyName) && !empty($this->{$propertyName})) {

                        $property = $this->{$propertyName};

                        $property = is_array($property) ? $property : array($property);

                        if (count($property) > 0) {

                                if ($type == self::MANY_MANY) {

                                    list($tableName, $rest) = explode("(", $foreignKey, 2);

                                    list($myKey, $theirKey) = explode(",", $rest, 2);

                                    $theirKey = substr($theirKey, 0, -1);

                                    foreach ($property as $child) {

                                        if ($action)

                                            Yii::app()->db->createCommand()->delete($tableName, "`" . $myKey . "`=:myKey AND `" . $theirKey . "`=:theirKey", array(":myKey" => $myPk, ":theirKey" => $child));

                                        else

                                            Yii::app()->db->createCommand("INSERT IGNORE INTO `" . $tableName . "` (`" . $myKey . "`,`" . $theirKey . "`) VALUES (:myKey,:theirKey)")->execute(array(":myKey" => $myPk, ":theirKey" => $child));

                                    }

                                } else {

     

                                    foreach ($property as $child) {

                                        $secondaryAttribute = $propertyName . 'Attribute';

                                        $secondaryAttribute = property_exists($this, $secondaryAttribute) ? $this->{$secondaryAttribute} : FALSE;

                                        $model = $action ? $modelName::model()->findByPk($child) : new $modelName; 

                                        if ($model !== null) {

                                            if ($action)

                                                $model->setAttribute($foreignKey, null);

                                            else {

                                                $model->setAttribute($foreignKey, (int) $myPk);

                                                if ($secondaryAttribute)

                                                    $model->setAttribute($secondaryAttribute, $child);

                                            }

                                            if(!$model->save()){

                                                $this->addErrors($model->getErrors());

                                                $state = FALSE;

                                            }

                                            

                                        }

                                    }

                                }

                        }

                    }

                }

            }

        }

        return $state;

    }



How this works:

All you need to do is add 2 attributes to your model. For example if I want to add provinces (with model name "CountryProvince") to a country, and remove them if they exist in an array/string/int:


    public $addCountryProvince;

        public $removeCountryProvince;

The relation in this model is the following:


    public function relations() {

        return array(

            'provinces' => array(self::HAS_MANY, 'CountryProvince', 'countryId'),

        );

    }

Now, if we would like to add an existing countryProvince, addCountryProvince can be either an array or a string/int containing the primary key of the record. The same goes for removeCountryProvince.

Ofcourse there always might be a scenario where we would like to create a new item and at the same time add it to our relation. In this case we have to set another variable in our model:


    protected $addCountryProvinceAttribute = "name";

This tells our script what attribute in the relation model the value corresponds with. In this example our CountryProvince has an attribute called "name", this should be filled with the value in the addCountryProvince variable and then saved.

If the above method returns any errors they will be added to the current model.

Note: The only drawback is that the code expects you to specify the model’s primary key first in MANY_MANY relations. For example if the model representing a “Post” has many categories and categories have many post, this would be the correct relation specification (from the Yii wiki):


return array(

            'categories'=>array(self::MANY_MANY, 'Category',

                'tbl_post_category(post_id, category_id)'),

        );

In the category model you would switch this around to read: ‘category_id,post_id’

Installation:

This code belongs in the parent of your ActiveRecord, I recommend extending the CActiveRecord, put this code in there and then extend all of your models on this class.

Note: If you overwrite the before/afterSave methods in your model, do not forget to call parent::beforeSave() or afterSave().

For all the guru’s, tips/tricks/advice is very welcome!!

Cool idea! There are three behaviors in the extension repository that do exactly that:

http://www.yiiframework.com/extension/cadvancedarbehavior/

http://www.yiiframework.com/extension/advancedrelationsbehavior/

http://www.yiiframework.com/extension/save-relations-ar-behavior/

I hope we see the best of this 4 implementations in the core of ActiveRecord in Yii2.0 ;)

Thank you for your reply Thyseus, indeed I found a couple of behaviors. One of them is yourse if I am not mistaken!

The reason I coded it myself was that I did not want all these extra lines of code, as in 90% of the cases I want it to do exactly as described. If there is an exception you can just override the before or afterSave functions and preprocess anything you’d like.

Also the behaviors lack the option to insert a new record, which was really what I needed.

I’d recommend to check this one: https://github.com/yiiext/with-related-behavior

Nice, missed that one. It actually covers all the functionality and more, will use it in the next project. Thanks

Missed that one too and it’s the best one a used so far.

I added some delete functionality to it on line 249




if ($data === null)

{

     if($model->markedDeleted && !$model->isNewRecord)

          $model->delete();

     else if(!$model->markedDeleted)

          $model->getIsNewRecord() ? $model->insert() : $model->update(); //$model->save(false) still runs beforeValidate

}



I extended CActiveRecord so all my ActiveRecords would have the markedDeleted boolean.

I wanted to override the save() function so that it would never call validate() when calling save(false) and make the save() function call delete() when the markedDeleted boolean was set true

Maybe an idea for some Yii core features?