├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Error │ └── MissingColumnException.php ├── Model │ └── Table │ │ └── SoftDeleteTrait.php └── ORM │ └── Query.php └── tests ├── Fixture ├── PostsFixture.php ├── PostsTagsFixture.php ├── TagsFixture.php └── UsersFixture.php ├── TestCase └── Model │ └── Table │ └── SoftDeleteTraitTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vagrant 3 | composer.lock 4 | Vagrantfile 5 | vendor 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | 8 | install: 9 | - composer self-update 10 | - composer install --dev 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakeSoftDelete plugin for CakePHP 2 | 3 | [![Build status](https://api.travis-ci.org/PGBI/cakephp3-soft-delete.png?branch=master)](https://travis-ci.org/PGBI/cakephp3-soft-delete) 4 | 5 | ## Purpose 6 | 7 | This Cakephp plugin enables you to make your models soft deletable. 8 | When soft deleting an entity, it is not actually removed from your database. Instead, a `deleted` timestamp is set on the record. 9 | 10 | ## Requirements 11 | 12 | This plugins has been developed for cakephp 3.x. 13 | 14 | ## Installation 15 | 16 | You can install this plugin into your CakePHP application using [composer](http://getcomposer.org). 17 | 18 | Update your composer file to include this plugin: 19 | 20 | ``` 21 | composer require pgbi/cakephp3-soft-delete "~1.0" 22 | ``` 23 | 24 | ## Configuration 25 | 26 | ### Load the plugin: 27 | ``` 28 | // In /config/bootstrap.php 29 | Plugin::load('SoftDelete'); 30 | ``` 31 | ### Make a model soft deleteable: 32 | 33 | Use the SoftDelete trait on your model Table class: 34 | 35 | ``` 36 | // in src/Model/Table/UsersTable.php 37 | ... 38 | use SoftDelete\Model\Table\SoftDeleteTrait; 39 | 40 | class UsersTable extends Table 41 | { 42 | use SoftDeleteTrait; 43 | ... 44 | ``` 45 | 46 | Your soft deletable model database table should have a field called `deleted` of type DateTime with NULL as default value. 47 | If you want to customise this field you can declare the field in your Table class. 48 | 49 | ```php 50 | // in src/Model/Table/UsersTable.php 51 | ... 52 | use SoftDelete\Model\Table\SoftDeleteTrait; 53 | 54 | class UsersTable extends Table 55 | { 56 | use SoftDeleteTrait; 57 | 58 | protected $softDeleteField = 'deleted_date'; 59 | ... 60 | ``` 61 | 62 | ## Use 63 | 64 | ### Soft deleting records 65 | 66 | `delete` and `deleteAll` functions will now soft delete records by populating `deleted` field with the date of the deletion. 67 | 68 | ```php 69 | // in src/Model/Table/UsersTable.php 70 | $this->delete($user); // $user entity is now soft deleted if UsersTable uses SoftDeleteTrait. 71 | ``` 72 | 73 | ### Restoring Soft deleted records 74 | 75 | To restore a soft deleted entity into an active state, use the `restore` method: 76 | 77 | ```php 78 | // in src/Model/Table/UsersTable.php 79 | // Let's suppose $user #1 is soft deleted. 80 | $user = $this->Users->find('all', ['withDeleted'])->where('id', 1)->first(); 81 | $this->restore($user); // $user #1 is now restored. 82 | ``` 83 | 84 | ### Finding records 85 | 86 | `find`, `get` or dynamic finders (such as `findById`) will only return non soft deleted records. 87 | To also return soft deleted records, `$options` must contain `'withDeleted'`. Example: 88 | 89 | ```php 90 | // in src/Model/Table/UsersTable.php 91 | $nonSoftDeletedRecords = $this->find('all'); 92 | $allRecords = $this->find('all', ['withDeleted']); 93 | ``` 94 | 95 | ### Hard deleting records 96 | 97 | To hard delete a single entity: 98 | ```php 99 | // in src/Model/Table/UsersTable.php 100 | $user = $this->get($userId); 101 | $success = $this->hardDelete($user); 102 | ``` 103 | 104 | To mass hard delete records that were soft deleted before a given date, you can use hardDeleteAll($date): 105 | 106 | ``` 107 | // in src/Model/Table/UsersTable.php 108 | $date = new \DateTime('some date'); 109 | $affectedRowsCount = $this->hardDeleteAll($date); 110 | ``` 111 | 112 | ## Soft deleting & associations 113 | 114 | Associations are correctly handled by SoftDelete plugin. 115 | 116 | 1. Soft deletion will be cascaded to related models as usual. If related models also use SoftDelete Trait, they will be soft deleted. 117 | 2. Soft deletes records will be excluded from counter caches. 118 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pgbi/cakephp3-soft-delete", 3 | "description": "SoftDelete plugin for CakePHP", 4 | "keywords": ["cakephp", "cakephp 3", "plugin", "soft", "delete", "deletable"], 5 | "homepage": "https://github.com/pgbi/cakephp3-soft-delete", 6 | "type": "cakephp-plugin", 7 | "license": "MIT", 8 | "support": { 9 | "source": "https://github.com/pgbi/cakephp3-soft-delete" 10 | }, 11 | "require": { 12 | "php": ">=5.4", 13 | "cakephp/plugin-installer": "*", 14 | "cakephp/cakephp": "~3.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "*" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "SoftDelete\\": "src" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "SoftDelete\\Test\\": "tests", 27 | "Cake\\Test\\": "./vendor/cakephp/cakephp/tests" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./tests/TestCase 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ./vendor/ 36 | ./vendor/ 37 | 38 | ./tests/ 39 | ./tests/ 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Error/MissingColumnException.php: -------------------------------------------------------------------------------- 1 | softDeleteField)) { 20 | $field = $this->softDeleteField; 21 | } else { 22 | $field = 'deleted'; 23 | } 24 | 25 | if ($this->schema()->column($field) === null) { 26 | throw new MissingColumnException( 27 | __('Configured field `{0}` is missing from the table `{1}`.', 28 | $field, 29 | $this->alias() 30 | ) 31 | ); 32 | } 33 | 34 | return $field; 35 | } 36 | 37 | public function query() 38 | { 39 | return new Query($this->connection(), $this); 40 | } 41 | 42 | /** 43 | * Perform the delete operation. 44 | * 45 | * Will soft delete the entity provided. Will remove rows from any 46 | * dependent associations, and clear out join tables for BelongsToMany associations. 47 | * 48 | * @param \Cake\DataSource\EntityInterface $entity The entity to soft delete. 49 | * @param \ArrayObject $options The options for the delete. 50 | * @throws \InvalidArgumentException if there are no primary key values of the 51 | * passed entity 52 | * @return bool success 53 | */ 54 | protected function _processDelete($entity, $options) 55 | { 56 | if ($entity->isNew()) { 57 | return false; 58 | } 59 | 60 | $primaryKey = (array)$this->primaryKey(); 61 | if (!$entity->has($primaryKey)) { 62 | $msg = 'Deleting requires all primary key values.'; 63 | throw new \InvalidArgumentException($msg); 64 | } 65 | 66 | if ($options['checkRules'] && !$this->checkRules($entity, RulesChecker::DELETE, $options)) { 67 | return false; 68 | } 69 | 70 | $event = $this->dispatchEvent('Model.beforeDelete', [ 71 | 'entity' => $entity, 72 | 'options' => $options 73 | ]); 74 | 75 | if ($event->isStopped()) { 76 | return $event->result; 77 | } 78 | 79 | $this->_associations->cascadeDelete( 80 | $entity, 81 | ['_primary' => false] + $options->getArrayCopy() 82 | ); 83 | 84 | $query = $this->query(); 85 | $conditions = (array)$entity->extract($primaryKey); 86 | $statement = $query->update() 87 | ->set([$this->getSoftDeleteField() => date('Y-m-d H:i:s')]) 88 | ->where($conditions) 89 | ->execute(); 90 | 91 | $success = $statement->rowCount() > 0; 92 | if (!$success) { 93 | return $success; 94 | } 95 | 96 | $this->dispatchEvent('Model.afterDelete', [ 97 | 'entity' => $entity, 98 | 'options' => $options 99 | ]); 100 | 101 | return $success; 102 | } 103 | 104 | /** 105 | * Soft deletes all records matching `$conditions`. 106 | * @return int number of affected rows. 107 | */ 108 | public function deleteAll($conditions) 109 | { 110 | $query = $this->query() 111 | ->update() 112 | ->set([$this->getSoftDeleteField() => date('Y-m-d H:i:s')]) 113 | ->where($conditions); 114 | $statement = $query->execute(); 115 | $statement->closeCursor(); 116 | return $statement->rowCount(); 117 | } 118 | 119 | /** 120 | * Hard deletes the given $entity. 121 | * @return bool true in case of success, false otherwise. 122 | */ 123 | public function hardDelete(EntityInterface $entity) 124 | { 125 | if(!$this->delete($entity)) { 126 | return false; 127 | } 128 | $primaryKey = (array)$this->primaryKey(); 129 | $query = $this->query(); 130 | $conditions = (array)$entity->extract($primaryKey); 131 | $statement = $query->delete() 132 | ->where($conditions) 133 | ->execute(); 134 | 135 | $success = $statement->rowCount() > 0; 136 | if (!$success) { 137 | return $success; 138 | } 139 | 140 | return $success; 141 | } 142 | 143 | /** 144 | * Hard deletes all records that were soft deleted before a given date. 145 | * @param \DateTime $until Date until which soft deleted records must be hard deleted. 146 | * @return int number of affected rows. 147 | */ 148 | public function hardDeleteAll(\Datetime $until) 149 | { 150 | $query = $this->query() 151 | ->delete() 152 | ->where([ 153 | $this->getSoftDeleteField() . ' IS NOT NULL', 154 | $this->getSoftDeleteField() . ' <=' => $until->format('Y-m-d H:i:s') 155 | ]); 156 | $statement = $query->execute(); 157 | $statement->closeCursor(); 158 | return $statement->rowCount(); 159 | } 160 | 161 | /** 162 | * Restore a soft deleted entity into an active state. 163 | * @param EntityInterface $entity Entity to be restored. 164 | * @return bool true in case of success, false otherwise. 165 | */ 166 | public function restore(EntityInterface $entity) 167 | { 168 | $softDeleteField = $this->getSoftDeleteField(); 169 | $entity->$softDeleteField = null; 170 | return $this->save($entity); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/ORM/Query.php: -------------------------------------------------------------------------------- 1 | _beforeFindFired && $this->_type === 'select') { 23 | parent::triggerBeforeFind(); 24 | 25 | $repository = $this->repository(); 26 | $options = $this->getOptions(); 27 | 28 | if (!is_array($options) || !in_array('withDeleted', $options)) { 29 | $aliasedField = $repository->aliasField($repository->getSoftDeleteField()); 30 | $this->andWhere($aliasedField . ' IS NULL'); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Fixture/PostsFixture.php: -------------------------------------------------------------------------------- 1 | belongsTo('Users'); 17 | $this->belongsToMany('Tags'); 18 | $this->addBehavior('CounterCache', ['Users' => ['posts_count']]); 19 | } 20 | } 21 | 22 | 23 | class PostsFixture extends TestFixture { 24 | 25 | public $fields = [ 26 | 'id' => ['type' => 'integer'], 27 | 'user_id' => ['type' => 'integer', 'default' => '0', 'null' => false], 28 | 'deleted' => ['type' => 'datetime', 'default' => null, 'null' => true], 29 | '_constraints' => [ 30 | 'primary' => ['type' => 'primary', 'columns' => ['id']] 31 | ] 32 | ]; 33 | public $records = [ 34 | [ 35 | 'id' => 1, 36 | 'user_id' => 1, 37 | 'deleted' => null, 38 | ], 39 | [ 40 | 'id' => 2, 41 | 'user_id' => 1, 42 | 'deleted' => null, 43 | ], 44 | ]; 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/Fixture/PostsTagsFixture.php: -------------------------------------------------------------------------------- 1 | belongsTo('Tags'); 17 | $this->belongsTo('Posts'); 18 | } 19 | } 20 | 21 | 22 | class PostsTagsFixture extends TestFixture { 23 | 24 | public $fields = [ 25 | 'id' => ['type' => 'integer'], 26 | 'post_id' => ['type' => 'integer'], 27 | 'tag_id' => ['type' => 'integer'], 28 | 'deleted' => ['type' => 'datetime', 'default' => null, 'null' => true], 29 | '_constraints' => [ 30 | 'primary' => ['type' => 'primary', 'columns' => ['id']] 31 | ] 32 | ]; 33 | 34 | public $records = [ 35 | [ 36 | 'id' => 1, 37 | 'post_id' => 1, 38 | 'tag_id' => 1, 39 | 'deleted' => null, 40 | ], 41 | [ 42 | 'id' => 2, 43 | 'post_id' => 1, 44 | 'tag_id' => 2, 45 | 'deleted' => '2015-05-18 15:04:00', 46 | ], 47 | ]; 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/Fixture/TagsFixture.php: -------------------------------------------------------------------------------- 1 | belongsToMany('Posts', [ 19 | 'through' => 'PostsTags', 20 | 'joinTable' => 'posts_tags', 21 | 'foreignKey' => 'tag_id', 22 | 'targetForeignKey' => 'post_id' 23 | ]); 24 | $this->hasMany('PostsTags'); 25 | } 26 | } 27 | 28 | 29 | class TagsFixture extends TestFixture { 30 | 31 | public $fields = [ 32 | 'id' => ['type' => 'integer'], 33 | 'name' => ['type' => 'string'], 34 | 'deleted_date' => ['type' => 'datetime', 'default' => null, 'null' => true], 35 | '_constraints' => [ 36 | 'primary' => ['type' => 'primary', 'columns' => ['id']] 37 | ] 38 | ]; 39 | 40 | public $records = [ 41 | [ 42 | 'id' => 1, 43 | 'name' => 'Cat', 44 | 'deleted_date' => null, 45 | ], 46 | [ 47 | 'id' => 2, 48 | 'name' => 'Dog', 49 | 'deleted_date' => null, 50 | ], 51 | [ 52 | 'id' => 3, 53 | 'name' => 'Fish', 54 | 'deleted_date' => '2015-04-15 09:46:00', 55 | ] 56 | ]; 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /tests/Fixture/UsersFixture.php: -------------------------------------------------------------------------------- 1 | hasMany('Posts', [ 17 | 'dependent' => true, 18 | 'cascadeCallbacks' => true, 19 | ]); 20 | } 21 | } 22 | 23 | class UsersFixture extends TestFixture { 24 | 25 | public $fields = [ 26 | 'id' => ['type' => 'integer'], 27 | 'posts_count' => ['type' => 'integer', 'default' => '0', 'null' => false], 28 | 'deleted' => ['type' => 'datetime', 'default' => null, 'null' => true], 29 | '_constraints' => [ 30 | 'primary' => ['type' => 'primary', 'columns' => ['id']] 31 | ] 32 | ]; 33 | public $records = [ 34 | [ 35 | 'id' => 1, 36 | 'deleted' => null, 37 | 'posts_count' => 2 38 | ], 39 | [ 40 | 'id' => 2, 41 | 'deleted' => null, 42 | 'posts_count' => 0 43 | ], 44 | ]; 45 | } 46 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Table/SoftDeleteTraitTest.php: -------------------------------------------------------------------------------- 1 | usersTable = TableRegistry::get('Users', ['className' => 'SoftDelete\Test\Fixture\UsersTable']); 39 | $this->postsTable = TableRegistry::get('Posts', ['className' => 'SoftDelete\Test\Fixture\PostsTable']); 40 | $this->tagsTable = TableRegistry::get('Tags', ['className' => 'SoftDelete\Test\Fixture\TagsTable']); 41 | $this->postsTagsTable = TableRegistry::get('PostsTags', ['className' => 'SoftDelete\Test\Fixture\PostsTagsTable']); 42 | } 43 | 44 | /** 45 | * tearDown method 46 | * 47 | * @return void 48 | */ 49 | public function tearDown() 50 | { 51 | unset($this->usersTable); 52 | unset($this->postsTable); 53 | unset($this->tagsTable); 54 | parent::tearDown(); 55 | } 56 | 57 | /** 58 | * Tests that a soft deleted entities is not found when calling Table::find() 59 | */ 60 | public function testFind() 61 | { 62 | $user = $this->usersTable->get(1); 63 | $user->deleted = date('Y-m-d H:i:s'); 64 | $this->usersTable->save($user); 65 | 66 | $user = $this->usersTable->find()->where(['id' => 1])->first(); 67 | $this->assertEquals(null, $user); 68 | 69 | } 70 | 71 | /** 72 | * Tests that a soft deleted entities is not found when calling Table::findByXXX() 73 | */ 74 | public function testDynamicFinder() 75 | { 76 | $user = $this->usersTable->get(1); 77 | $user->deleted = date('Y-m-d H:i:s'); 78 | $this->usersTable->save($user); 79 | 80 | $user = $this->usersTable->findById(1)->first(); 81 | $this->assertEquals(null, $user); 82 | } 83 | 84 | public function testFindWithOrWhere() 85 | { 86 | $user = $this->usersTable->get(2); 87 | $this->usersTable->delete($user); 88 | 89 | $query = $this->usersTable->find()->where(['id' => 1])->orWhere(['id' => 2]); 90 | $this->assertEquals(1, $query->count()); 91 | } 92 | 93 | /** 94 | * Tests that soft deleted records in join table are taken into account when retrieving 95 | * an entity with a belongsToManyAssociation. 96 | */ 97 | public function testFindBelongsToMany() 98 | { 99 | $post = $this->postsTable->findById(1)->contain('Tags')->first(); 100 | $this->assertEquals(1, count($post->tags)); 101 | } 102 | 103 | /** 104 | * Test that entities matching a soft deleted associated record are not returned when using $query->matching(). 105 | */ 106 | public function testFindMatching() 107 | { 108 | $users = $this->usersTable->find() 109 | ->matching('Posts', function($q) { 110 | return $q->where(['Posts.id' => 1]); 111 | }); 112 | $this->assertEquals(1, $users->count()); 113 | 114 | $post = $this->postsTable->get(1); 115 | $this->postsTable->delete($post); 116 | 117 | $posts = $this->postsTable->find('all', ['withDeleted'])->where(['id' => 1]); 118 | $this->assertEquals(1, $posts->count()); 119 | 120 | $users = $this->usersTable->find() 121 | ->matching('Posts', function($q) { 122 | return $q->where(['Posts.id' => 1]); 123 | }); 124 | $this->assertEquals(0, $users->count()); 125 | } 126 | 127 | 128 | /** 129 | * Tests that Table::deleteAll() does not hard delete 130 | */ 131 | public function testDeleteAll() 132 | { 133 | $this->usersTable->deleteAll([]); 134 | $this->assertEquals(0, $this->usersTable->find()->count()); 135 | $this->assertNotEquals(0, $this->usersTable->find('all', ['withDeleted'])->count()); 136 | 137 | $this->postsTable->deleteAll([]); 138 | $this->assertEquals(0, $this->postsTable->find()->count()); 139 | $this->assertNotEquals(0, $this->postsTable->find('all', ['withDeleted'])->count()); 140 | } 141 | 142 | /** 143 | * Tests that Table::delete() does not hard delete. 144 | */ 145 | public function testDelete() 146 | { 147 | $user = $this->usersTable->get(1); 148 | $this->usersTable->delete($user); 149 | $user = $this->usersTable->findById(1)->first(); 150 | $this->assertEquals(null, $user); 151 | 152 | $user = $this->usersTable->find('all', ['withDeleted'])->where(['id' => 1])->first(); 153 | $this->assertNotEquals(null, $user); 154 | $this->assertNotEquals(null, $user->deleted); 155 | } 156 | 157 | /** 158 | * Tests that soft deleting an entity also soft deletes its belonging entities. 159 | */ 160 | public function testHasManyAssociation() 161 | { 162 | $user = $this->usersTable->get(1); 163 | $this->usersTable->delete($user); 164 | 165 | $count = $this->postsTable->find()->where(['user_id' => 1])->count(); 166 | $this->assertEquals(0, $count); 167 | 168 | $count = $this->postsTable->find('all', ['withDeleted'])->where(['user_id' => 1])->count(); 169 | $this->assertEquals(2, $count); 170 | } 171 | 172 | /** 173 | * Tests that soft deleting affects counters the same way that hard deleting. 174 | */ 175 | public function testCounterCache() 176 | { 177 | $post = $this->postsTable->get(1); 178 | $this->postsTable->delete($post); 179 | $this->assertNotEquals(null, $this->postsTable->find('all', ['withDeleted'])->where(['id' => 1])->first()); 180 | $this->assertEquals(null, $this->postsTable->findById(1)->first()); 181 | 182 | $user = $this->usersTable->get(1); 183 | $this->assertEquals(1, $user->posts_count); 184 | } 185 | 186 | public function testHardDelete() 187 | { 188 | $user = $this->usersTable->get(1); 189 | $this->usersTable->hardDelete($user); 190 | $user = $this->usersTable->findById(1)->first(); 191 | $this->assertEquals(null, $user); 192 | 193 | $user = $this->usersTable->find('all', ['withDeleted'])->where(['id' => 1])->first(); 194 | $this->assertEquals(null, $user); 195 | } 196 | 197 | /** 198 | * Tests hardDeleteAll. 199 | */ 200 | public function testHardDeleteAll() 201 | { 202 | $affectedRows = $this->postsTable->hardDeleteAll(new \DateTime('now')); 203 | $this->assertEquals(0, $affectedRows); 204 | 205 | $postsRowsCount = $this->postsTable->find('all', ['withDeleted'])->count(); 206 | 207 | $this->postsTable->delete($this->postsTable->get(1)); 208 | $affectedRows = $this->postsTable->hardDeleteAll(new \DateTime('now')); 209 | $this->assertEquals(1, $affectedRows); 210 | 211 | $newpostsRowsCount = $this->postsTable->find('all', ['withDeleted'])->count(); 212 | $this->assertEquals($postsRowsCount - 1, $newpostsRowsCount); 213 | } 214 | 215 | /** 216 | * Using a table with a custom soft delete field, ensure we can still filter 217 | * the found results properly. 218 | * 219 | * @return void 220 | */ 221 | public function testFindingWithCustomField() 222 | { 223 | $query = $this->tagsTable->find(); 224 | $this->assertEquals(2, $query->count()); 225 | 226 | $query = $this->tagsTable->find('all', ['withDeleted' => true]); 227 | $this->assertEquals(3, $query->count()); 228 | } 229 | 230 | /** 231 | * Ensure that when deleting a record which has a custom field defined in 232 | * the table, that it is still soft deleted. 233 | * 234 | * @return void 235 | */ 236 | public function testDeleteWithCustomField() 237 | { 238 | $tag = $this->tagsTable->get(1); 239 | $this->tagsTable->delete($tag); 240 | 241 | $query = $this->tagsTable->find(); 242 | $this->assertEquals(1, $query->count()); 243 | } 244 | 245 | /** 246 | * With a custom soft delete field ensure that a soft deleted record can 247 | * still be permanently removed. 248 | * 249 | * @return void 250 | */ 251 | public function testHardDeleteWithCustomField() 252 | { 253 | $tag = $this->tagsTable->find('all', ['withDeleted']) 254 | ->where(['id' => 2]) 255 | ->first(); 256 | 257 | $this->tagsTable->hardDelete($tag); 258 | 259 | $tag = $this->tagsTable->find('all', ['withDeleted']) 260 | ->where(['id' => 2]) 261 | ->first(); 262 | 263 | $this->assertEquals(null, $tag); 264 | } 265 | 266 | /** 267 | * Test soft deleting and restoring a record. 268 | * @return void 269 | */ 270 | public function testRestore() 271 | { 272 | $user = $this->usersTable->findById(1)->first(); 273 | $this->assertNotNull($user); 274 | $this->usersTable->delete($user); 275 | $user = $this->usersTable->findById(1)->first(); 276 | $this->assertNull($user); 277 | 278 | $user = $this->usersTable->find('all', ['withDeleted'])->where(['id' => 1])->first(); 279 | $this->usersTable->restore($user); 280 | $user = $this->usersTable->findById(1)->first(); 281 | $this->assertNotNull($user); 282 | } 283 | 284 | /** 285 | * When a configured field is missing from the table, an exception should be thrown 286 | * 287 | * @expectedException \SoftDelete\Error\MissingColumnException 288 | */ 289 | public function testMissingColumn() 290 | { 291 | $this->postsTable->softDeleteField = 'foo'; 292 | $post = $this->postsTable->get(1); 293 | $this->postsTable->delete($post); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | register(); 28 | $loader->addNamespace('Cake\Test\Fixture', ROOT . '/vendor/cakephp/cakephp/tests/Fixture'); 29 | require_once CORE_PATH . 'config/bootstrap.php'; 30 | date_default_timezone_set('UTC'); 31 | mb_internal_encoding('UTF-8'); 32 | Configure::write('debug', true); 33 | Configure::write('App', [ 34 | 'namespace' => 'App', 35 | 'encoding' => 'UTF-8', 36 | 'base' => false, 37 | 'baseUrl' => false, 38 | 'dir' => 'src', 39 | 'webroot' => 'webroot', 40 | 'wwwRoot' => APP . 'webroot', 41 | 'fullBaseUrl' => 'http://localhost', 42 | 'imageBaseUrl' => 'img/', 43 | 'jsBaseUrl' => 'js/', 44 | 'cssBaseUrl' => 'css/', 45 | 'paths' => [ 46 | 'plugins' => [APP . 'Plugin' . DS], 47 | 'templates' => [APP . 'Template' . DS] 48 | ] 49 | ]); 50 | 51 | Configure::write('Session', [ 52 | 'defaults' => 'php' 53 | ]); 54 | 55 | Cache::config([ 56 | '_cake_core_' => [ 57 | 'engine' => 'File', 58 | 'prefix' => 'cake_core_', 59 | 'serialize' => true 60 | ], 61 | '_cake_model_' => [ 62 | 'engine' => 'File', 63 | 'prefix' => 'cake_model_', 64 | 'serialize' => true 65 | ], 66 | 'default' => [ 67 | 'engine' => 'File', 68 | 'prefix' => 'default_', 69 | 'serialize' => true 70 | ] 71 | ]); 72 | 73 | // Ensure default test connection is defined 74 | if (!getenv('db_class')) { 75 | putenv('db_class=Cake\Database\Driver\Sqlite'); 76 | putenv('db_dsn=sqlite::memory:'); 77 | } 78 | 79 | ConnectionManager::config('test', [ 80 | 'className' => 'Cake\Database\Connection', 81 | 'driver' => getenv('db_class'), 82 | 'dsn' => getenv('db_dsn'), 83 | 'database' => getenv('db_database'), 84 | 'username' => getenv('db_login'), 85 | 'password' => getenv('db_password'), 86 | 'timezone' => 'UTC' 87 | ]); 88 | 89 | Log::config([ 90 | 'debug' => [ 91 | 'engine' => 'Cake\Log\Engine\FileLog', 92 | 'levels' => ['notice', 'info', 'debug'], 93 | 'file' => 'debug', 94 | ], 95 | 'error' => [ 96 | 'engine' => 'Cake\Log\Engine\FileLog', 97 | 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], 98 | 'file' => 'error', 99 | ] 100 | ]); 101 | --------------------------------------------------------------------------------