Browse Source

Implementation of enhancement #15: Allow saving extra columns to junction table

tags/1.4.0
Alban Jubert 7 years ago
parent
commit
94a441c974
  1. 69
      src/SaveRelationsBehavior.php
  2. 38
      tests/SaveRelationsBehaviorTest.php
  3. 22
      tests/models/Project.php
  4. 44
      tests/models/Tag.php

69
src/SaveRelationsBehavior.php

@ -29,13 +29,14 @@ class SaveRelationsBehavior extends Behavior
private $_transaction;
private $_relationsScenario = [];
private $_relationsExtraColumns = [];
//private $_relationsCascadeDelete = []; //TODO
public function init()
{
parent::init();
$allowedProperties = ['scenario'];
$allowedProperties = ['scenario', 'extraColumns'];
foreach ($this->relations as $key => $value) {
if (is_int($key)) {
$this->_relations[] = $value;
@ -345,8 +346,6 @@ class SaveRelationsBehavior extends Behavior
$model = $this->owner;
$this->_relationsSaveStarted = true;
try {
foreach ($this->_relations as $relationName) {
if (array_key_exists($relationName, $this->_oldRelationValue)) { // Relation was not set, do nothing...
Yii::trace("Linking {$relationName} relation", __METHOD__);
@ -354,7 +353,6 @@ class SaveRelationsBehavior extends Behavior
if ($relation->multiple === true) { // Has many relation
// Process new relations
$existingRecords = [];
/** @var BaseActiveRecord $relationModel */
foreach ($model->{$relationName} as $i => $relationModel) {
if ($relationModel->isNewRecord) {
@ -367,7 +365,8 @@ class SaveRelationsBehavior extends Behavior
throw new DbException("Related record {$pettyRelationName} could not be saved.");
}
}
$model->link($relationName, $relationModel);
$junctionTableColumns = $this->_getJunctionTableColumns($relationName, $relationModel);
$model->link($relationName, $relationModel, $junctionTableColumns);
} else {
$existingRecords[] = $relationModel;
}
@ -381,21 +380,31 @@ class SaveRelationsBehavior extends Behavior
}
}
}
$junctionTablePropertiesUsed = array_key_exists($relationName, $this->_relationsExtraColumns);
// Process existing added and deleted relations
list($addedPks, $deletedPks) = $this->_computePkDiff($this->_oldRelationValue[$relationName], $existingRecords);
list($addedPks, $deletedPks) = $this->_computePkDiff(
$this->_oldRelationValue[$relationName],
$existingRecords,
$junctionTablePropertiesUsed
);
// Deleted relations
$initialModels = ArrayHelper::index($this->_oldRelationValue[$relationName], function (BaseActiveRecord $model) {
return implode("-", $model->getPrimaryKey(true));
});
$initialRelations = $model->{$relationName};
foreach ($deletedPks as $key) {
$model->unlink($relationName, $initialModels[$key], true);
}
// Added relations
$actualModels = ArrayHelper::index($model->{$relationName}, function (BaseActiveRecord $model) {
return implode("-", $model->getPrimaryKey(true));
});
$actualModels = ArrayHelper::index(
$junctionTablePropertiesUsed ? $initialRelations : $model->{$relationName},
function (BaseActiveRecord $model) {
return implode("-", $model->getPrimaryKey(true));
}
);
foreach ($addedPks as $key) {
$model->link($relationName, $actualModels[$key]);
$junctionTableColumns = $this->_getJunctionTableColumns($relationName, $actualModels[$key]);
$model->link($relationName, $actualModels[$key], $junctionTableColumns);
}
} else { // Has one relation
if ($this->_oldRelationValue[$relationName] !== $model->{$relationName}) {
@ -449,12 +458,39 @@ class SaveRelationsBehavior extends Behavior
}
/**
* Return array of columns to save to the junction table for a related model having a many-to-many relation.
* @param string $relationName
* @param BaseActiveRecord $model
* @return array
*/
private function _getJunctionTableColumns($relationName, $model)
{
$junctionTableColumns = [];
if (array_key_exists($relationName, $this->_relationsExtraColumns)) {
if (is_callable($this->_relationsExtraColumns[$relationName])) {
$junctionTableColumns = $this->_relationsExtraColumns[$relationName]($model);
} elseif (is_array($this->_relationsExtraColumns[$relationName])) {
$junctionTableColumns = $this->_relationsExtraColumns[$relationName];
}
if (!is_array($junctionTableColumns)) {
throw new RuntimeException(
'Junction table columns definition must return an array, got ' . gettype($junctionTableColumns)
);
}
}
return $junctionTableColumns;
}
/**
* Compute the difference between two set of records using primary keys "tokens"
* If third parameter is set to true all initial related records will be marked for removal even if their
* properties did not change. This can be handy in a many-to-many relation involving a junction table.
* @param BaseActiveRecord[] $initialRelations
* @param BaseActiveRecord[] $updatedRelations
* @param bool $forceSave
* @return array
*/
private function _computePkDiff($initialRelations, $updatedRelations)
private function _computePkDiff($initialRelations, $updatedRelations, $forceSave = false)
{
// Compute differences between initial relations and the current ones
$oldPks = ArrayHelper::getColumn($initialRelations, function (BaseActiveRecord $model) {
@ -463,9 +499,14 @@ class SaveRelationsBehavior extends Behavior
$newPks = ArrayHelper::getColumn($updatedRelations, function (BaseActiveRecord $model) {
return implode("-", $model->getPrimaryKey(true));
});
$identicalPks = array_intersect($oldPks, $newPks);
$addedPks = array_values(array_diff($newPks, $identicalPks));
$deletedPks = array_values(array_diff($oldPks, $identicalPks));
if ($forceSave) {
$addedPks = $newPks;
$deletedPks = $oldPks;
} else {
$identicalPks = array_intersect($oldPks, $newPks);
$addedPks = array_values(array_diff($newPks, $identicalPks));
$deletedPks = array_values(array_diff($oldPks, $identicalPks));
}
return [$addedPks, $deletedPks];
}

38
tests/SaveRelationsBehaviorTest.php

@ -2,7 +2,6 @@
namespace tests;
use lhs\Yii2SaveRelationsBehavior\SaveRelationsBehavior;
use RuntimeException;
use tests\models\Company;
@ -11,6 +10,7 @@ use tests\models\DummyModelParent;
use tests\models\Link;
use tests\models\Project;
use tests\models\ProjectNoTransactions;
use tests\models\Tag;
use tests\models\User;
use Yii;
use yii\base\Model;
@ -35,6 +35,8 @@ class SaveRelationsBehaviorTest extends \PHPUnit_Framework_TestCase
$db->createCommand()->dropTable('company')->execute();
$db->createCommand()->dropTable('link_type')->execute();
$db->createCommand()->dropTable('link')->execute();
$db->createCommand()->dropTable('project_tags')->execute();
$db->createCommand()->dropTable('tags')->execute();
$db->createCommand()->dropTable('project_link')->execute();
$db->createCommand()->dropTable('dummy')->execute();
parent::tearDown();
@ -79,6 +81,17 @@ class SaveRelationsBehaviorTest extends \PHPUnit_Framework_TestCase
'PRIMARY KEY(language, name)'
])->execute();
$db->createCommand()->createTable('tags', [
'id' => $migration->primaryKey(),
'name' => $migration->string()->notNull()->unique()
])->execute();
$db->createCommand()->createTable('project_tags', [
'project_id' => $migration->integer()->notNull(),
'tag_id' => $migration->integer()->notNull(),
'order' => $migration->integer()->notNull()
])->execute();
$db->createCommand()->createTable('link_type', [
'id' => $migration->primaryKey(),
'name' => $migration->string()->notNull()->unique()
@ -381,6 +394,27 @@ class SaveRelationsBehaviorTest extends \PHPUnit_Framework_TestCase
);
}
public function testSaveNewManyRelationJunctionTableColumnsShouldSucceed()
{
$project = Project::findOne(1);
$firstTag = new Tag();
$firstTag->name = 'Tag One';
$firstTag->setOrder(1);
$secondTag = new Tag();
$secondTag->name = 'Tag Two';
$secondTag->setOrder(3);
$project->tags = [
$firstTag,
$secondTag
];
$this->assertTrue($project->save(), 'Project could not be saved');
$this->assertCount(2, $project->tags, 'Project should have 2 tags after assignment');
$firstTagJunctionTableColumns = (new \yii\db\Query())->from('project_tags')->where(['tag_id' => $firstTag->id])->one();
$secondTagJunctionTableColumns = (new \yii\db\Query())->from('project_tags')->where(['tag_id' => $secondTag->id])->one();
$this->assertEquals($firstTag->getOrder(), $firstTagJunctionTableColumns['order']);
$this->assertEquals($secondTag->getOrder(), $secondTagJunctionTableColumns['order']);
}
public function testSaveMixedRelationsShouldSucceed()
{
$project = new Project();
@ -582,7 +616,7 @@ class SaveRelationsBehaviorTest extends \PHPUnit_Framework_TestCase
$project->loadRelations($data);
$this->assertFalse($project->save(), 'Project could be saved');
$data = [
'Link' => [
'Link' => [
[
'language' => 'en',
'name' => 'yii',

22
tests/models/Project.php

@ -22,7 +22,19 @@ class Project extends \yii\db\ActiveRecord
return [
'saveRelations' => [
'class' => SaveRelationsBehavior::className(),
'relations' => ['company', 'users', 'links' => ['scenario' => Link::SCENARIO_FIRST]]
'relations' => [
'company',
'users',
'links' => ['scenario' => Link::SCENARIO_FIRST],
'tags' => [
'extraColumns' => function ($model) {
/** @var $model Tag */
return [
'order' => $model->order
];
}
]
],
],
];
}
@ -90,4 +102,12 @@ class Project extends \yii\db\ActiveRecord
return $this->hasMany(Link::className(), ['language' => 'language', 'name' => 'name'])->via('projectLinks');
}
/**
* @return ActiveQuery
*/
public function getTags()
{
return $this->hasMany(Tag::className(), ['id' => 'tag_id'])->viaTable('project_tags', ['project_id' => 'id']);
}
}

44
tests/models/Tag.php

@ -0,0 +1,44 @@
<?php
namespace tests\models;
class Tag extends \yii\db\ActiveRecord
{
/** @var int */
protected $order;
/**
* @inheritdoc
*/
public static function tableName()
{
return 'tags';
}
/**
* @param int $order
*/
public function setOrder($order)
{
$this->order = $order;
}
/**
* @return int
*/
public function getOrder()
{
return $this->order;
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['name'], 'required'],
[['name'], 'string']
];
}
}
Loading…
Cancel
Save