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!!