There are many similar solutions in today's web applications. Leading products, such as Google's Gmail, are defining nice UI patterns. One of these is soft delete . Instead of a permanent deletion with tons of confirmations, Gmail allows the user to immediately mark messages as deleted and then easily undo it. The same behavior can be applied to any object, such as blog posts, comments, and so on.
Let's create a behavior that will allow marking models as deleted, restoring models, and selecting not yet deleted models, deleted models, and all models. In this recipe we'll follow a test-driven development approach to plan the behavior and test if the implementation is correct.
Carry out the following steps:
post
table to your database:CREATE TABLE `post` ( `id` int(11) NOT NULL auto_increment, `text` text, `title` varchar(255) default NULL, `is_deleted` tinyint(1) NOT NULL default '0', PRIMARY KEY (`id`) )
protected/config/main.php
).test
application has the same settings (protected/config/test.php
).fixture
component in the test
application settings.Post
model.Post
model in protected/tests/fixtures/post.php
:<?php return array( array( 'id' => 1, 'title' => 'post1', 'text' => 'post1', 'is_deleted' => 0, ), array( 'id' => 2, 'title' => 'post2', 'text' => 'post2', 'is_deleted' => 1, ), array( 'id' => 3, 'title' => 'post3', 'text' => 'post3', 'is_deleted' => 0, ), array( 'id' => 4, 'title' => 'post4', 'text' => 'post4', 'is_deleted' => 1, ), array( 'id' => 5, 'title' => 'post5', 'text' => 'post5', 'is_deleted' => 0, ), );
protected/tests/unit/soft_delete/SoftDeleteBehaviorTest.php
:<?php class SoftDeleteBehaviorTest extends CDbTestCase { protected $fixtures = array( 'post' => 'Post', ); function testRemoved() { $postCount = Post::model()->removed()->count(); $this->assertEquals(2, $postCount); } function testNotRemoved() { $postCount = Post::model()->notRemoved()->count(); $this->assertEquals(3, $postCount); } function testRemove() { $post = Post::model()->findByPk(1); $post->remove()->save(); $this->assertNull(Post::model()->notRemoved()->findByPk(1)); } function testRestore() { $post = Post::model()->findByPk(2); $post->restore()->save(); $this->assertNotNull(Post::model()->notRemoved()->findByPk(2)); } function testIsDeleted() { $post = Post::model()->findByPk(1); $this->assertFalse($post->isRemoved()); $post = Post::model()->findByPk(2); $this->assertTrue($post->isRemoved()); } }
protected/extensions
named soft_delete
. Under this directory create SoftDeleteBehavior.php
. Let's attach the behavior to the Post
model first:class Post extends CActiveRecord { // … public function behaviors() { return array( 'softDelete' => array( 'class' => 'ext.soft_delete.SoftDeleteBehavior' ), ); } // …
protected/extensions/soft_delete/SoftDeleteBehavior.php
:<?php class SoftDeleteBehavior extends CActiveRecordBehavior { public $flagField = 'is_deleted'; public function remove() { $this->getOwner()->{$this->flagField} = 1; return $this->getOwner(); } public function restore() { $this->getOwner()->{$this->flagField} = 0; return $this->getOwner(); } public function notRemoved() { $criteria = $this->getOwner()->getDbCriteria(); $criteria->compare($this->flagField, 0); return $this->getOwner(); } public function removed() { $criteria = $this->getOwner()->getDbCriteria(); $criteria->compare($this->flagField, 1); return $this->getOwner(); } public function isRemoved() { return (boolean)$this->getOwner()->{$this->flagField}; } }
Let's start with the test case. Since we want to use a set of models, we are defining fixtures. Fixture set is put into the DB each time the test method is executed. To use fixtures, the test class should be inherited from CDbTestCase
and have protected $fixtures
declared:
protected $fixtures = array( 'post' => 'Post', );
In the preceding definition, post
is the name of the file with fixture definitions and Post
is the name of the model to which fixtures will be applied.
First, we are testing removed
and notRemoved
custom named scopes. We should limit the find result to removed items only, and then to non-removed items. Since we know which data we will get from fixtures, we can test to get a count of removed and non-removed items such as the following:
$postCount = Post::model()->removed()->count(); $this->assertEquals(2, $postCount);
Then we test the remove
and restore
methods. The following is the remove
method test:
$post = Post::model()->findByPk(1); $post->remove()->save(); $this->assertNull(Post::model()->notRemoved()->findByPk(1));
We are getting the item by id
, removing it, and then trying to get it again using the notRemoved
named scope. Since it's removed, we should get null
as the result.
Finally, we are testing the isRemoved
method that just returns the corresponding column value as Boolean.
Now, let's move to the interesting implementation details. Since we are implementing the Active Record model behavior, we need to extend from CActiveRecordBehavior
. In behavior we can add our own methods that will be mixed into the model that behavior is attached to. We are using it to add the remove
/restore
/isRemoved
methods and removed
/notRemoved
named scopes:
public function remove() { $this->getOwner()->{$this->flagField} = 1; return $this->getOwner(); } public function removed() { $criteria = $this->getOwner()->getDbCriteria(); $criteria->compare($this->flagField, 1); return $this->getOwner(); }
In both methods we are using the getOwner
method to get the object the behavior is attached to. In our case it's a model, so we can work with its data or change its finder criteria. We are returning the model instance to allow chained method calls such as the following:
$post->remove()->save();
There are a couple of things that should be mentioned in this recipe.
Sometimes we need to get some more flexibility in a behavior, such as reacting to model events. Both
CActiveRecordBehavior
and CModelBehavior
are adding event-like methods we can override to handle model events. For example, if we need to handle cascade delete in a behavior, we can do it by overriding the afterDelete
method.
Behavior can be attached not only to a model but also to any component. Each behavior inherits from the CBehavior
class so we can use its methods as follows:
To learn more about behaviors, refer to the following API pages:
3.141.244.153