diff --git a/README.md b/README.md index f271b6b..b2cc16c 100644 --- a/README.md +++ b/README.md @@ -248,4 +248,184 @@ $allItems = Item::find() $allItems = Item::find() ->filterDeleted('all') // filter all records, preventing default "not deleted" condition ->all(); // returns all records -``` \ No newline at end of file +``` + + +## Smart deletion + +Usually "soft" deleting feature is used to prevent the database history loss, ensuring data, which been in use and +perhaps have a references or dependencies, is kept in the system. However, sometimes actual deleting is allowed for +such data as well. +For example: usually user account records should not be deleted but only marked as "inactive", however if you browse +through users list and found accounts, which has been registered long ago, but don't have at least single log-in in the +system, these records have no value for the history and can be removed from database to save disk space. + +You can make "soft" deletion to be "smart" and detect, if the record can be removed from the database or only marked as "deleted". +This can be done via `\yii1tech\ar\softdelete\SoftDeleteBehavior::$allowDeleteCallback`. For example: + +```php + [ + 'class' => SoftDeleteBehavior::class, + 'softDeleteAttributeValues' => [ + 'is_deleted' => true + ], + 'allowDeleteCallback' => function ($user) { + return $user->last_login_date === null; // allow to delete user, if he has never logged in + } + ], + ]; + } +} + +$user = User::model()->find('last_login_date IS NULL'); +$user->softDelete(); // removes the record!!! + +$user = User::find()->find('last_login_date IS NOT NULL'); +$user->softDelete(); // marks record as "deleted" +``` + +`\yii1tech\ar\softdelete\SoftDeleteBehavior::$allowDeleteCallback` logic is applied in case `\yii1tech\ar\softdelete\SoftDeleteBehavior::$replaceRegularDelete` +is enabled as well. + + +## Handling foreign key constraints + +In case of usage of the relational database, which supports foreign keys, like MySQL, PostgreSQL etc., "soft" deletion +is widely used for keeping foreign keys consistence. For example: if user performs a purchase at the online shop, information +about this purchase should remain in the system for the future bookkeeping. The DDL for such data structure may look like +following one: + +```sql +CREATE TABLE `customer` +( + `id` integer NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `address` varchar(64) NOT NULL, + `phone` varchar(20) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE InnoDB; + +CREATE TABLE `purchase` +( + `id` integer NOT NULL AUTO_INCREMENT, + `customer_id` integer NOT NULL, + `item_id` integer NOT NULL, + `amount` integer NOT NULL, + PRIMARY KEY (`id`) + FOREIGN KEY (`customer_id`) REFERENCES `customer` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, + FOREIGN KEY (`item_id`) REFERENCES `item` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, +) ENGINE InnoDB; +``` + +Thus, while set up a foreign key from 'purchase' to 'user', 'ON DELETE RESTRICT' mode is used. So on attempt to delete +a user record, which have at least one purchase, a database error will occur. However, if user record have no external +reference, it can be deleted. + +Usage of `\yii1tech\ar\softdelete\SoftDeleteBehavior::$allowDeleteCallback` for such use case is not very practical. +It will require performing extra queries to determine, if external references exist or not, eliminating the benefits of +the foreign keys database feature. + +Method `\yii1tech\ar\softdelete\SoftDeleteBehavior::safeDelete()` attempts to invoke regular `CBaseActiveRecord::delete()` +method, and, if it fails with exception, falls back to `yii1tech\ar\softdelete\SoftDeleteBehavior::softDelete()`. + +```php +findByPk(15); +var_dump(count($customer->purchases)); // outputs; "1" +$customer->safeDelete(); // performs "soft" delete! +var_dump($customer->isDeleted) // outputs: "true" + +// if there is NO foreign key reference : +$customer = Customer::model()->findByPk(53); +var_dump(count($customer->purchases)); // outputs; "0" +$customer->safeDelete(); // performs actual delete! +$customer = Customer::model()->findByPk(53); +var_dump($customer); // outputs: "null" +``` + +By default `safeDelete()` method catches `\CDbException` exception, which means soft deleting will be +performed on foreign constraint violation DB exception. You may specify another exception class here to customize fallback +error level. For example: usage of `\Throwable` will cause soft-delete fallback on any error during regular deleting. + + +## Record restoration + +At some point you may want to "restore" records, which have been marked as "deleted" in the past. +You may use `restore()` method for this: + +```php +findByPk($id); +$item->softDelete(); // mark record as "deleted" + +$item = Item::model()->findByPk($id); +$item->restore(); // restore record +var_dump($item->is_deleted); // outputs "false" +``` + +By default, attribute values, which should be applied for record restoration are automatically detected from `\yii1tech\ar\softdelete\SoftDeleteBehavior::$softDeleteAttributeValues`, +however it is better you specify them explicitly via `\yii1tech\ar\softdelete\SoftDeleteBehavior::$restoreAttributeValues`. + +> Tip: if you enable `\yii1tech\ar\softdelete\SoftDeleteBehavior::$useRestoreAttributeValuesAsDefaults`, attribute values, + which marks restored record, will be automatically applied at new record insertion. + + +## Events + +By default `\yii1tech\ar\softdelete\SoftDeleteBehavior::softDelete()` triggers `\CActiveRecord::onBeforeDelete` +and `\CActiveRecord::onAfterDelete` events in the same way they are triggered at regular `delete()`. + +Also `\yii1tech\ar\softdelete\SoftDeleteBehavior` allows you to hook on soft-delete process defining specific methods at the owner ActiveRecord class: + +- `beforeSoftDelete()` - triggered before "soft" delete is made. +- `afterSoftDelete()` - triggered after "soft" delete is made. +- `beforeRestore()` - triggered before record is restored from "deleted" state. +- `afterRestore()` - triggered after record is restored from "deleted" state. + +For example: + +```php + [ + 'class' => SoftDeleteBehavior::class, + // ... + ], + ]; + } + + public function beforeSoftDelete(): bool + { + $this->deleted_at = time(); // log the deletion date + + return true; + } + + public function beforeRestore(): bool + { + return $this->deleted_at > (time() - 3600); // allow restoration only for the records, being deleted during last hour + } +} +```