From 08f3ff8bd1c0ce2a16359b3eb75e3c4839dbacc0 Mon Sep 17 00:00:00 2001 From: Alban Jubert Date: Sat, 7 Apr 2018 17:22:54 +0200 Subject: [PATCH] Added ability to delete related record after owner model delete --- src/SaveRelationsBehavior.php | 47 +++++++++++++++++++++++++++++++++- tests/SaveRelationsBehaviorTest.php | 50 +++++++++++++++++++++++++++++++++++++ tests/bootstrap.php | 20 +++++++-------- tests/models/Project.php | 5 ++-- tests/models/ProjectLink.php | 11 ++++++++ tests/models/User.php | 2 +- 6 files changed, 121 insertions(+), 14 deletions(-) diff --git a/src/SaveRelationsBehavior.php b/src/SaveRelationsBehavior.php index a1f5e1a..f5789a6 100644 --- a/src/SaveRelationsBehavior.php +++ b/src/SaveRelationsBehavior.php @@ -15,6 +15,7 @@ use yii\db\Exception as DbException; use yii\db\Transaction; use yii\helpers\ArrayHelper; use yii\helpers\Inflector; +use yii\helpers\VarDumper; /** * This Active Record Behavior allows to validate and save the Model relations when the save() method is invoked. @@ -28,12 +29,14 @@ class SaveRelationsBehavior extends Behavior private $_relations = []; private $_oldRelationValue = []; // Store initial relations value private $_newRelationValue = []; // Store update relations value + private $_relationsToDelete = []; private $_relationsSaveStarted = false; private $_transaction; private $_relationsScenario = []; private $_relationsExtraColumns = []; + private $_relationsCascadeDelete = []; /** * @param $relationName @@ -53,7 +56,7 @@ class SaveRelationsBehavior extends Behavior public function init() { parent::init(); - $allowedProperties = ['scenario', 'extraColumns']; + $allowedProperties = ['scenario', 'extraColumns', 'cascadeDelete']; foreach ($this->relations as $key => $value) { if (is_int($key)) { $this->_relations[] = $value; @@ -81,6 +84,8 @@ class SaveRelationsBehavior extends Behavior BaseActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate', BaseActiveRecord::EVENT_AFTER_INSERT => 'afterSave', BaseActiveRecord::EVENT_AFTER_UPDATE => 'afterSave', + BaseActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete', + BaseActiveRecord::EVENT_AFTER_DELETE => 'afterDelete' ]; } @@ -386,6 +391,46 @@ class SaveRelationsBehavior extends Behavior } /** + * Get the list of owner model relations in order to be able to delete them after its deletion + */ + public function beforeDelete() + { + $model = $this->owner; + foreach ($this->_relationsCascadeDelete as $relationName => $params) { + if ($params === true) { + $relation = $model->getRelation($relationName); + if (!empty($model->{$relationName})) { + if ($relation->multiple === true) { // Has many relation + $this->_relationsToDelete = ArrayHelper::merge($this->_relationsToDelete, $model->{$relationName}); + } else { + $this->_relationsToDelete[] = $model->{$relationName}; + } + } + } + } + } + + /** + * Delete related models marked as to be deleted + * @throws Exception + */ + public function afterDelete() + { + /** @var BaseActiveRecord $modelToDelete */ + foreach ($this->_relationsToDelete as $modelToDelete) { + try { + if (!$modelToDelete->delete()) { + throw new DbException('Could not delete the related record: ' . $modelToDelete::className() . '(' . VarDumper::dumpAsString($modelToDelete->primaryKey) . ')'); + } + } catch (Exception $e) { + Yii::warning(get_class($e) . ' was thrown while deleting related records during afterDelete event: ' . $e->getMessage(), __METHOD__); + $this->_rollback(); + throw $e; + } + } + } + + /** * 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 diff --git a/tests/SaveRelationsBehaviorTest.php b/tests/SaveRelationsBehaviorTest.php index f084c7c..a3b996a 100644 --- a/tests/SaveRelationsBehaviorTest.php +++ b/tests/SaveRelationsBehaviorTest.php @@ -8,6 +8,7 @@ use tests\models\DummyModel; use tests\models\DummyModelParent; use tests\models\Link; use tests\models\Project; +use tests\models\ProjectLink; use tests\models\ProjectNoTransactions; use tests\models\Tag; use tests\models\User; @@ -142,6 +143,13 @@ class SaveRelationsBehaviorTest extends \PHPUnit_Framework_TestCase [4, 'Jonathan Ive', 1] ])->execute(); + $db->createCommand()->batchInsert('user_profile', ['user_id', 'bio'], [ + [1, 'Steven Paul Jobs (February 24, 1955 – October 5, 2011) was an American entrepreneur, business magnate, inventor, and industrial designer. He was the chairman, chief executive officer (CEO), and co-founder of Apple Inc.; CEO and majority shareholder of Pixar; a member of The Walt Disney Company\'s board of directors following its acquisition of Pixar; and the founder, chairman, and CEO of NeXT.'], + [2, 'William Henry Gates III (born October 28, 1955) is an American business magnate, investor, author, philanthropist, and co-founder of the Microsoft Corporation along with Paul Allen.'], + [3, 'Timothy Donald Cook (born November 1, 1960) is an American business executive, industrial engineer, and developer. Cook is the Chief Executive Officer of Apple Inc., previously serving as the company\'s Chief Operating Officer, under its founder Steve Jobs.'], + [4, 'Sir Jonathan Paul "Jony" Ive, KBE (born 27 February 1967), is an English industrial designer who is currently the chief design officer (CDO) of Apple and chancellor of the Royal College of Art in London.'] + ])->execute(); + $db->createCommand()->batchInsert('project', ['id', 'name', 'company_id'], [ [1, 'Mac OS X', 1], [2, 'Windows 10', 2] @@ -725,4 +733,46 @@ class SaveRelationsBehaviorTest extends \PHPUnit_Framework_TestCase $user->userProfile->bio = 'Lawrence Edward Page (born March 26, 1973) is an American computer scientist and Internet entrepreneur who co-founded Google with Sergey Brin.'; $this->assertTrue($user->save(), 'User could not be saved'); } + + public function testDeleteRelatedHasOneShouldSucceed() + { + User::findOne(1)->delete(); + $this->assertNull(UserProfile::findOne(1), 'Related user profile was not deleted'); + $this->assertNotNull(UserProfile::findOne(2), 'Unrelated user profile was deleted'); + } + + public function testDeleteRelatedHasManyShouldSucceed() + { + Project::findOne(1)->delete(); + $this->assertCount(0, ProjectLink::find()->where(['project_id' => 1])->all(), 'Related project links were not deleted'); + } + + public function testDeleteRelatedWithErrorShouldThrowAnException() + { + $this->setExpectedException('\yii\db\Exception'); + $project = Project::findOne(1); + foreach ($project->projectLinks as $projectLink) { + $projectLink->blockDelete = true; + } + $this->assertFalse($project->delete(), 'Project could be deleted'); + } + + public function testSaveProjectWithCompanyWithUserShouldSucceed() + { + // Test for cascading save relations + $project = new Project(); + $project->name = "Cartoon"; + $company = new Company(); + $company->name = 'ACME'; + $user = new User(); + $user->username = "Bugs Bunny"; + $company->users = $user; + $project->company = $company; + $this->assertTrue($project->save(), 'Project could not be saved'); + $this->assertEquals('ACME', $project->company->name, 'Project company\'s name is wrong'); + $this->assertCount(1, $project->company->users, 'Count of related users is wrong'); + $this->assertEquals('Bugs Bunny', $project->company->users[0]->username, 'Company user\'s name is wrong'); + $this->assertFalse($project->company->isNewRecord, 'Company record should be saved'); + $this->assertFalse($project->company->users[0]->isNewRecord, 'Company Users records should be saved'); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3aea0f7..e2b5360 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -15,19 +15,19 @@ new \yii\console\Application([ 'id' => 'unit', 'basePath' => __DIR__, 'vendorPath' => dirname(__DIR__) . '/vendor', -// 'bootstrap' => ['log'], + 'bootstrap' => ['log'], 'components' => [ - 'db' => [ + 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'sqlite::memory:', ], -// 'log' => [ -// 'targets' => [ -// [ -// 'class' => 'yii\log\FileTarget', -// 'categories' => ['yii\db\*', 'lhs\Yii2SaveRelationsBehavior\*'] -// ], -// ] -// ], + 'log' => [ + 'targets' => [ + [ + 'class' => 'yii\log\FileTarget', + 'categories' => ['yii\db\*', 'lhs\Yii2SaveRelationsBehavior\*'] + ], + ] + ], ] ]); diff --git a/tests/models/Project.php b/tests/models/Project.php index c91a772..5077c30 100644 --- a/tests/models/Project.php +++ b/tests/models/Project.php @@ -25,8 +25,9 @@ class Project extends \yii\db\ActiveRecord 'relations' => [ 'company', 'users', - 'links' => ['scenario' => Link::SCENARIO_FIRST], - 'tags' => [ + 'links' => ['scenario' => Link::SCENARIO_FIRST], + 'projectLinks' => ['cascadeDelete' => true], + 'tags' => [ 'extraColumns' => function ($model) { /** @var $model Tag */ return [ diff --git a/tests/models/ProjectLink.php b/tests/models/ProjectLink.php index e20f6db..8957e43 100644 --- a/tests/models/ProjectLink.php +++ b/tests/models/ProjectLink.php @@ -5,6 +5,8 @@ namespace tests\models; class ProjectLink extends \yii\db\ActiveRecord { + public $blockDelete = false; + /** * @inheritdoc */ @@ -24,4 +26,13 @@ class ProjectLink extends \yii\db\ActiveRecord ]; } + public function beforeDelete() + { + if ($this->blockDelete === true) { + return false; + } else { + return parent::beforeDelete(); + } + } + } diff --git a/tests/models/User.php b/tests/models/User.php index 1759942..e69912b 100644 --- a/tests/models/User.php +++ b/tests/models/User.php @@ -22,7 +22,7 @@ class User extends \yii\db\ActiveRecord return [ 'saveRelations' => [ 'class' => SaveRelationsBehavior::className(), - 'relations' => ['userProfile', 'company'] + 'relations' => ['userProfile' => ['cascadeDelete' => true], 'company'] ], ]; }