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 $_transaction;
private $_relationsScenario = []; private $_relationsScenario = [];
private $_relationsExtraColumns = [];
//private $_relationsCascadeDelete = []; //TODO //private $_relationsCascadeDelete = []; //TODO
public function init() public function init()
{ {
parent::init(); parent::init();
$allowedProperties = ['scenario']; $allowedProperties = ['scenario', 'extraColumns'];
foreach ($this->relations as $key => $value) { foreach ($this->relations as $key => $value) {
if (is_int($key)) { if (is_int($key)) {
$this->_relations[] = $value; $this->_relations[] = $value;
@ -345,8 +346,6 @@ class SaveRelationsBehavior extends Behavior
$model = $this->owner; $model = $this->owner;
$this->_relationsSaveStarted = true; $this->_relationsSaveStarted = true;
try { try {
foreach ($this->_relations as $relationName) { foreach ($this->_relations as $relationName) {
if (array_key_exists($relationName, $this->_oldRelationValue)) { // Relation was not set, do nothing... if (array_key_exists($relationName, $this->_oldRelationValue)) { // Relation was not set, do nothing...
Yii::trace("Linking {$relationName} relation", __METHOD__); Yii::trace("Linking {$relationName} relation", __METHOD__);
@ -354,7 +353,6 @@ class SaveRelationsBehavior extends Behavior
if ($relation->multiple === true) { // Has many relation if ($relation->multiple === true) { // Has many relation
// Process new relations // Process new relations
$existingRecords = []; $existingRecords = [];
/** @var BaseActiveRecord $relationModel */ /** @var BaseActiveRecord $relationModel */
foreach ($model->{$relationName} as $i => $relationModel) { foreach ($model->{$relationName} as $i => $relationModel) {
if ($relationModel->isNewRecord) { if ($relationModel->isNewRecord) {
@ -367,7 +365,8 @@ class SaveRelationsBehavior extends Behavior
throw new DbException("Related record {$pettyRelationName} could not be saved."); 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 { } else {
$existingRecords[] = $relationModel; $existingRecords[] = $relationModel;
} }
@ -381,21 +380,31 @@ class SaveRelationsBehavior extends Behavior
} }
} }
} }
$junctionTablePropertiesUsed = array_key_exists($relationName, $this->_relationsExtraColumns);
// Process existing added and deleted relations // 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 // Deleted relations
$initialModels = ArrayHelper::index($this->_oldRelationValue[$relationName], function (BaseActiveRecord $model) { $initialModels = ArrayHelper::index($this->_oldRelationValue[$relationName], function (BaseActiveRecord $model) {
return implode("-", $model->getPrimaryKey(true)); return implode("-", $model->getPrimaryKey(true));
}); });
$initialRelations = $model->{$relationName};
foreach ($deletedPks as $key) { foreach ($deletedPks as $key) {
$model->unlink($relationName, $initialModels[$key], true); $model->unlink($relationName, $initialModels[$key], true);
} }
// Added relations // Added relations
$actualModels = ArrayHelper::index($model->{$relationName}, function (BaseActiveRecord $model) { $actualModels = ArrayHelper::index(
return implode("-", $model->getPrimaryKey(true)); $junctionTablePropertiesUsed ? $initialRelations : $model->{$relationName},
}); function (BaseActiveRecord $model) {
return implode("-", $model->getPrimaryKey(true));
}
);
foreach ($addedPks as $key) { 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 } else { // Has one relation
if ($this->_oldRelationValue[$relationName] !== $model->{$relationName}) { 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" * 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[] $initialRelations
* @param BaseActiveRecord[] $updatedRelations * @param BaseActiveRecord[] $updatedRelations
* @param bool $forceSave
* @return array * @return array
*/ */
private function _computePkDiff($initialRelations, $updatedRelations) private function _computePkDiff($initialRelations, $updatedRelations, $forceSave = false)
{ {
// Compute differences between initial relations and the current ones // Compute differences between initial relations and the current ones
$oldPks = ArrayHelper::getColumn($initialRelations, function (BaseActiveRecord $model) { $oldPks = ArrayHelper::getColumn($initialRelations, function (BaseActiveRecord $model) {
@ -463,9 +499,14 @@ class SaveRelationsBehavior extends Behavior
$newPks = ArrayHelper::getColumn($updatedRelations, function (BaseActiveRecord $model) { $newPks = ArrayHelper::getColumn($updatedRelations, function (BaseActiveRecord $model) {
return implode("-", $model->getPrimaryKey(true)); return implode("-", $model->getPrimaryKey(true));
}); });
$identicalPks = array_intersect($oldPks, $newPks); if ($forceSave) {
$addedPks = array_values(array_diff($newPks, $identicalPks)); $addedPks = $newPks;
$deletedPks = array_values(array_diff($oldPks, $identicalPks)); $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]; return [$addedPks, $deletedPks];
} }

38
tests/SaveRelationsBehaviorTest.php

@ -2,7 +2,6 @@
namespace tests; namespace tests;
use lhs\Yii2SaveRelationsBehavior\SaveRelationsBehavior; use lhs\Yii2SaveRelationsBehavior\SaveRelationsBehavior;
use RuntimeException; use RuntimeException;
use tests\models\Company; use tests\models\Company;
@ -11,6 +10,7 @@ use tests\models\DummyModelParent;
use tests\models\Link; use tests\models\Link;
use tests\models\Project; use tests\models\Project;
use tests\models\ProjectNoTransactions; use tests\models\ProjectNoTransactions;
use tests\models\Tag;
use tests\models\User; use tests\models\User;
use Yii; use Yii;
use yii\base\Model; use yii\base\Model;
@ -35,6 +35,8 @@ class SaveRelationsBehaviorTest extends \PHPUnit_Framework_TestCase
$db->createCommand()->dropTable('company')->execute(); $db->createCommand()->dropTable('company')->execute();
$db->createCommand()->dropTable('link_type')->execute(); $db->createCommand()->dropTable('link_type')->execute();
$db->createCommand()->dropTable('link')->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('project_link')->execute();
$db->createCommand()->dropTable('dummy')->execute(); $db->createCommand()->dropTable('dummy')->execute();
parent::tearDown(); parent::tearDown();
@ -79,6 +81,17 @@ class SaveRelationsBehaviorTest extends \PHPUnit_Framework_TestCase
'PRIMARY KEY(language, name)' 'PRIMARY KEY(language, name)'
])->execute(); ])->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', [ $db->createCommand()->createTable('link_type', [
'id' => $migration->primaryKey(), 'id' => $migration->primaryKey(),
'name' => $migration->string()->notNull()->unique() '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() public function testSaveMixedRelationsShouldSucceed()
{ {
$project = new Project(); $project = new Project();
@ -582,7 +616,7 @@ class SaveRelationsBehaviorTest extends \PHPUnit_Framework_TestCase
$project->loadRelations($data); $project->loadRelations($data);
$this->assertFalse($project->save(), 'Project could be saved'); $this->assertFalse($project->save(), 'Project could be saved');
$data = [ $data = [
'Link' => [ 'Link' => [
[ [
'language' => 'en', 'language' => 'en',
'name' => 'yii', 'name' => 'yii',

22
tests/models/Project.php

@ -22,7 +22,19 @@ class Project extends \yii\db\ActiveRecord
return [ return [
'saveRelations' => [ 'saveRelations' => [
'class' => SaveRelationsBehavior::className(), '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 $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