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