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 us 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, 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.
yii2-app-basic
application using the composer as described in the official guide at http://www.yiiframework.com/doc-2.0/guide-start-installation.html.config/db.php
. Make sure the test application uses the second database in tests/codeception/config/config.php
.<?php use yiidbMigration; class m160427_103115_create_post_table extends Migration { public function up() { $this->createTable('{{%post}}', [ 'id' => $this->primaryKey(), 'title' => $this->string()->notNull(), 'content_markdown' => $this->text(), 'content_html' => $this->text(), ]); } public function down() { $this->dropTable('{{%post}}'); } }
./yii migrate tests/codeception/bin/yii migrate
Post
model:<?php namespace appmodels; use appehaviorsMarkdownBehavior; use yiidbActiveRecord; /** * @property integer $id * @property string $title * @property string $content_markdown * @property string $content_html */ class Post extends ActiveRecord { public static function tableName() { return '{{%post}}'; } public function rules() { return [ [['title'], 'required'], [['content_markdown'], 'string'], [['title'], 'string', 'max' => 255], ]; } }
Let's prepare a test environment first starting with defining fixtures for the Post
model. Create the tests/codeception/unit/fixtures/PostFixture.php
file:
<?php namespace app estscodeceptionunitfixtures; use yii estActiveFixture; class PostFixture extends ActiveFixture { public $modelClass = 'appmodelsPost'; public $dataFile = '@tests/codeception/unit/fixtures/data/post.php'; }
tests/codeception/unit/fixtures/data/post.php
:<?php return [ [ 'id' => 1, 'title' => 'Post 1', 'content_markdown' => 'Stored *markdown* text 1', 'content_html' => "<p>Stored <em>markdown</em> text 1</p> ", ], ];
tests/codeception/unit/MarkdownBehaviorTest.php
:<?php namespace app estscodeceptionunit; use appmodelsPost; use app estscodeceptionunitfixturesPostFixture; use yiicodeceptionDbTestCase; class MarkdownBehaviorTest extends DbTestCase { public function testNewModelSave() { $post = new Post(); $post->title = 'Title'; $post->content_markdown = 'New *markdown* text'; $this->assertTrue($post->save()); $this->assertEquals("<p>New <em>markdown</em> text</p> ", $post->content_html); } public function testExistingModelSave() { $post = Post::findOne(1); $post->content_markdown = 'Other *markdown* text'; $this->assertTrue($post->save()); $this->assertEquals("<p>Other <em>markdown</em> text</p> ", $post->content_html); } public function fixtures() { return [ 'posts' => [ 'class' => PostFixture::className(), ] ]; } }
codecept run unit MarkdownBehaviorTest Ensure that tests has not passed: Codeception PHP Testing Framework v2.0.9 Powered by PHPUnit 4.8.27 by Sebastian Bergmann and contributors. Unit Tests (2) --------------------------------------------------------------------------- Trying to test ... MarkdownBehaviorTest::testNewModelSave Error Trying to test ... MarkdownBehaviorTest::testExistingModelSave Error --------------------------------------------------------------------------- Time: 289 ms, Memory: 16.75MB
behaviors
. Under this directory, create a MarkdownBehavior
class:<?php namespace appehaviors; use yiiaseBehavior; use yiiaseEvent; use yiiaseInvalidConfigException; use yiidbActiveRecord; use yiihelpersMarkdown; class MarkdownBehavior extends Behavior { public $sourceAttribute; public $targetAttribute; public function init() { if (empty($this->sourceAttribute) || empty($this->targetAttribute)) { throw new InvalidConfigException('Source and target must be set.'); } parent::init(); } public function events() { return [ ActiveRecord::EVENT_BEFORE_INSERT => 'onBeforeSave', ActiveRecord::EVENT_BEFORE_UPDATE => 'onBeforeSave', ]; } public function onBeforeSave(Event $event) { if ($this->owner->isAttributeChanged($this->sourceAttribute)) { $this->processContent(); } } private function processContent() { $model = $this->owner; $source = $model->{$this->sourceAttribute}; $model->{$this->targetAttribute} = Markdown::process($source); } }
class Post extends ActiveRecord { ... public function behaviors() { return [ 'markdown' => [ 'class' => MarkdownBehavior::className(), 'sourceAttribute' => 'content_markdown', 'targetAttribute' => 'content_html', ], ]; } }
Codeception PHP Testing Framework v2.0.9 Powered by PHPUnit 4.8.27 by Sebastian Bergmann and contributors. Unit Tests (2) --------------------------------------------------------------------------- Trying to test ... MarkdownBehaviorTest::testNewModelSave Ok Trying to test ... MarkdownBehaviorTest::testExistingModelSave Ok --------------------------------------------------------------------------- Time: 329 ms, Memory: 17.00MB
Let's start with the test case. Since we want to use a set of models, we are defining fixtures. A fixture set is put into the "database" each time the test method is executed.
We prepare unit tests for specifying how the behavior must work:
Now let's move to the interesting implementation details. In behavior, we can add our own methods, which will be mixed into the model that the behavior is attached to. Also, we can subscribe to the owner component events. We are using it to add an own listener:
public function events() { return [ ActiveRecord::EVENT_BEFORE_INSERT => 'onBeforeSave', ActiveRecord::EVENT_BEFORE_UPDATE => 'onBeforeSave', ]; }
Now we can implement this listener:
public function onBeforeSave(Event $event) { if ($this->owner->isAttributeChanged($this->sourceAttribute)) { $this->processContent(); } }
In all the methods, we can use the owner
property to get the object the behavior is attached to. In general, we can attach any behavior to our models, controllers, applications, and other components that extend the yiiaseComponent
class. Also, we can attach one behavior repeatedly to the model for processing different attributes:
class Post extends ActiveRecord { ... public function behaviors() { return [ [ 'class' => MarkdownBehavior::className(), 'sourceAttribute' => 'description_markdown', 'targetAttribute' => 'description_html', ], [ 'class' => MarkdownBehavior::className(), 'sourceAttribute' => 'content_markdown', 'targetAttribute' => 'content_html', ], ]; } }
Besides, we can extend the yiiaseAttributeBehavior
class like yiiehaviorsTimestampBehavior
for updating specified attributes for any events.
To learn more about behaviors and events, refer to the following pages:
For more information about the Markdown syntax, refer to http://daringfireball.net/projects/markdown/.
Also, refer to the Making extensions distribution-ready recipe of this chapter.
18.221.165.246