Skip to content

Commit 8d1f313

Browse files
Added behavior for checking referential constraints before deleting model
1 parent 21edbe9 commit 8d1f313

File tree

2 files changed

+114
-5
lines changed

2 files changed

+114
-5
lines changed

Result.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,13 @@ public function addError($message, $namedError = null, $options = []) {
9595
}
9696

9797
if ($namedError != null) {
98-
if ($options['prepend'] && is_array($this->errorMessages[$namedError]) && !empty($this->errorMessages[$namedError])) {
98+
if (!empty($options['prepend']) && is_array($this->errorMessages[$namedError]) && !empty($this->errorMessages[$namedError])) {
9999
array_unshift($this->errorMessages[$namedError], $message);
100100
} else {
101101
$this->errorMessages[$namedError][] = $message;
102102
}
103103
} else {
104-
if ($options['prepend'] && is_array($this->errorMessages['_generic']) && !empty($this->errorMessages['_generic'])) {
104+
if (!empty($options['prepend']) && is_array($this->errorMessages['_generic']) && !empty($this->errorMessages['_generic'])) {
105105
array_unshift($this->errorMessages['_generic'], $message);
106106
} else {
107107
$this->errorMessages['_generic'][] = $message;
@@ -141,15 +141,15 @@ public function addError($message, $namedError = null, $options = []) {
141141
*/
142142
public function addErrors($arrayMessages, $options = []) {
143143
if (empty($arrayMessages)) {
144-
if ($options['errorIfEmpty']) {
144+
if (!empty($options['errorIfEmpty'])) {
145145
$this->addError($options['errorIfEmpty']);
146146
}
147147
return;
148148
}
149149

150-
if ($options['prefix']) {
150+
if (!empty($options['prefix'])) {
151151
$arrayMessages = $this->augmentMessages($arrayMessages, $options['prefix'], 'prefix');
152-
} elseif ($options['suffix']) {
152+
} elseif (!empty($options['suffix'])) {
153153
$arrayMessages = $this->augmentMessages($arrayMessages, $options['suffix'], 'suffix');
154154
}
155155

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
namespace winternet\yii2\behaviors;
3+
4+
use yii\db\ActiveRecord;
5+
use yii\base\Behavior;
6+
7+
/**
8+
* [BETA version!] Checks for referential constraints before deleting a model
9+
*
10+
* Maybe this is not a foolproof solution as there could maybe be deeper levels of restrictions (in other tables) - we only check one level deep...
11+
*/
12+
13+
class CheckReferentialConstraintsBehavior extends Behavior {
14+
15+
/**
16+
* @var array : Array of relations names, eg. `client`, `posts`, `userRoles` which matches the model's methods `getClient()`, `getPosts()`, `getUserRoles()`
17+
*/
18+
public $relations = [];
19+
20+
public function events() {
21+
return [
22+
ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
23+
];
24+
}
25+
26+
public function beforeDelete($event) {
27+
if (preg_match("/dbname=([^;]+)/", $this->owner->getDb()->dsn, $match)) { //example DSN: `mysql:host=localhost;dbname=myprojectname`
28+
$databaseName = $match[1];
29+
} else {
30+
throw new \winternet\yii2\UserException('Failed to determine database name when checking referential constraints before deleting a model.');
31+
}
32+
33+
// Not needed for now at least
34+
/*
35+
$schema = $this->owner->getTableSchema();
36+
if (!empty($schema->foreignKeys)) {
37+
foreach ($schema->foreignKeys as $keyName => $details) {
38+
$table = $details[0];
39+
}
40+
}
41+
*/
42+
43+
if (true) {
44+
// Method 1
45+
46+
foreach ($this->relations as $relationName) {
47+
$relationMethod = 'get'. $relationName;
48+
if (method_exists($this->owner, $relationMethod)) {
49+
$relation = $this->owner->$relationMethod();
50+
$primaryTable = $this->owner::tableName();
51+
$relatedTableName = $relation->modelClass::tableName();
52+
// $primaryKey = current($relation->link); //eg. userID
53+
// $foreignKey = array_keys($relation->link)[0]; //eg. role_userID
54+
55+
// Find other tables that has this table as a foreign key and which prohibits automatic deletion of their records when we our record
56+
$constraints = $this->owner->getDb()->createCommand("SELECT * FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` WHERE CONSTRAINT_SCHEMA = :databaseName AND REFERENCED_TABLE_NAME = :primaryTable AND TABLE_NAME = :foreignKeyTable", ['databaseName' => $databaseName, 'primaryTable' => $primaryTable, 'foreignKeyTable' => $relatedTableName])->queryAll();
57+
if (empty($constraints)) {
58+
continue;
59+
} elseif (count($constraints) === 1) {
60+
if ($constraints[0]['DELETE_RULE'] === 'RESTRICT') {
61+
// Check if that other table has any records referring to our record
62+
$referencingModels = $this->owner->$relationName;
63+
if (!empty($referencingModels)) {
64+
$this->owner->addError($this->determineErrorAttribute(), \Yii::t('app', 'Cannot delete record as it is referenced by {relationName}.', ['relationName' => $relationName]));
65+
}
66+
}
67+
} else {
68+
throw new \winternet\yii2\UserException('Checking referential constraint currently only works when exactly one constraint is found.', ['Model' => get_class($this->owner), 'relationName' => $relationName, 'databaseName' => $databaseName, 'primaryTable' => $primaryTable, 'foreignKeyTable' => $relatedTableName]);
69+
}
70+
} else {
71+
throw new \winternet\yii2\UserException('Expected method name does not exist based on the given relation name when checking referential constraints before deleting a model.', ['Model' => get_class($this->owner), 'relationName' => $relationName, 'Method name' => $relationMethod]);
72+
}
73+
}
74+
75+
} else {
76+
// Method 2
77+
78+
throw new \winternet\yii2\UserException('This alternative method has not yet been implemented when checking referential constraints before deleting a model.');
79+
80+
// This method would use a join so you get the column names as well, not just the table names (but we have them in yii\db\TableSchema->foreignKeys though!)
81+
// But this is very slow - but is okay if it is really needed.
82+
83+
// Source: https://stackoverflow.com/questions/12734331/constraint-detail-from-information-schema-on-update-cascade-on-delete-restrict
84+
$sql = "SELECT tb1.CONSTRAINT_NAME, tb1.TABLE_NAME, tb1.COLUMN_NAME,
85+
tb1.REFERENCED_TABLE_NAME, tb1.REFERENCED_COLUMN_NAME,
86+
tb2.UPDATE_RULE, tb2.DELETE_RULE
87+
88+
FROM information_schema.`KEY_COLUMN_USAGE` AS tb1
89+
INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS AS tb2 ON
90+
tb1.CONSTRAINT_NAME = tb2.CONSTRAINT_NAME AND tb1.TABLE_NAME = tb2.TABLE_NAME
91+
WHERE table_schema = 'forskoleutvecklingnu' AND tb1.TABLE_NAME = 'main_organizations' AND referenced_column_name IS NOT NULL";
92+
}
93+
94+
if ($this->owner->hasErrors()) {
95+
$event->isValid = false;
96+
}
97+
}
98+
99+
public function determineErrorAttribute() {
100+
// Just use the first column we find
101+
$attributeName = $this->owner->activeAttributes()[0];
102+
103+
if (empty($attributeName)) {
104+
throw new \winternet\yii2\UserException('Failed to determine an attribute for assigning error message to when checking referential constraints before deleting a model.', ['Model' => get_class($this->owner)]);
105+
}
106+
107+
return (string) $attributeName;
108+
}
109+
}

0 commit comments

Comments
 (0)