创建模型行为

现在的web应用中,有许多相似的解决方案。龙头产品例如google的Gmail,有这两个的UI模式。其中一个就是软删除。不需要点击成吨的确认进行永久删除,Gmail允许我们将信息立刻标记为删除,然后可以很容易的撤销它。相同的行为可以应用于任何对象上,例如博客帖子、评论等等。

下边我们来创建一个行为,它将允许我们将模型标记为删除,选择还未删除的模型,删除模型,以及所有的模型。在本小节中,我们将会进行一个测试驱动的开发方法,来计划这个行为,并测试实现是否正确。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。
  2. 创建两个数据库分别用于工作和测试。
  3. 在你的主应用config/db.php中配置Yii来使用第一个数据库。确认测试应用使用第二个数据库tests/codeception/config/config.php
  4. 创建一个新的migration:
  1. <?php
  2. use yii\db\Migration;
  3. class m160427_103115_create_post_table extends Migration
  4. {
  5. public function up()
  6. {
  7. $this->createTable('{{%post}}', [
  8. 'id' => $this->primaryKey(),
  9. 'title' => $this->string()->notNull(),
  10. 'content_markdown' => $this->text(),
  11. 'content_html' => $this->text(),
  12. ]);
  13. }
  14. public function down()
  15. {
  16. $this->dropTable('{{%post}}');
  17. }
  18. }
  1. 应用migration到工作和测试数据库上:
  1. ./yii migrate
  2. tests/codeception/bin/yii migrate
  1. 创建Post模型:
  1. <?php
  2. namespace app\models;
  3. use app\behaviors\MarkdownBehavior;
  4. use yii\db\ActiveRecord;
  5. /**
  6. * @property integer $id
  7. * @property string $title
  8. * @property string $content_markdown
  9. * @property string $content_html
  10. */
  11. class Post extends ActiveRecord
  12. {
  13. public static function tableName()
  14. {
  15. return '{{%post}}';
  16. }
  17. public function rules()
  18. {
  19. return [
  20. [['title'], 'required'],
  21. [['content_markdown'], 'string'],
  22. [['title'], 'string', 'max' => 255],
  23. ];
  24. }
  25. }

如何做…

准备一个测试环境,为Post模型定义fixtures。创建文件tests/codeception/unit/fixtures/PostFixture.php

  1. <?php
  2. namespace app\tests\codeception\unit\fixtures;
  3. use yii\test\ActiveFixture;
  4. class PostFixture extends ActiveFixture
  5. {
  6. public $modelClass = 'app\models\Post';
  7. public $dataFile = '@tests/codeception/unit/fixtures/data/post.php';
  8. }
  1. 添加一个fixture数据到tests/codeception/unit/fixtures/data/post.php
  1. <?php
  2. return [
  3. [
  4. 'id' => 1,
  5. 'title' => 'Post 1',
  6. 'content_markdown' => 'Stored *markdown* text 1',
  7. 'content_html' => "<p>Stored <em>markdown</em> text 1</p>\n",
  8. ],
  9. ];
  1. 然后,我们需要创建一个测试用例,tests/codeception/unit/MarkdownBehaviorTest.php
  1. <?php
  2. namespace app\tests\codeception\unit;
  3. use app\models\Post;
  4. use app\tests\codeception\unit\fixtures\PostFixture;
  5. use yii\codeception\DbTestCase;
  6. class MarkdownBehaviorTest extends DbTestCase
  7. {
  8. public function testNewModelSave()
  9. {
  10. $post = new Post();
  11. $post->title = 'Title';
  12. $post->content_markdown = 'New *markdown* text';
  13. $this->assertTrue($post->save());
  14. $this->assertEquals("<p>New <em>markdown</em> text</p>\n", $post->content_html);
  15. }
  16. public function testExistingModelSave()
  17. {
  18. $post = Post::findOne(1);
  19. $post->content_markdown = 'Other *markdown* text';
  20. $this->assertTrue($post->save());
  21. $this->assertEquals("<p>Other <em>markdown</em> text</p>\n", $post->content_html);
  22. }
  23. public function fixtures()
  24. {
  25. return [
  26. 'posts' => [
  27. 'class' => PostFixture::className(),
  28. ]
  29. ];
  30. }
  31. }
  1. 运行单元测试:
  1. codecept run unit MarkdownBehaviorTest
  2. Ensure that tests has not passed:
  3. Codeception PHP Testing Framework v2.0.9
  4. Powered by PHPUnit 4.8.27 by Sebastian Bergmann and
  5. contributors.
  6. Unit Tests (2)
  7. ----------------------------------------------------------------
  8. -----------
  9. Trying to test ...
  10. MarkdownBehaviorTest::testNewModelSave Error
  11. Trying to test ...
  12. MarkdownBehaviorTest::testExistingModelSave Error
  13. ----------------------------------------------------------------
  14. -----------
  15. Time: 289 ms, Memory: 16.75MB
  1. 现在我们需要实现行为,将它附加到模型上,并确保测试通过。创建一个新的文件夹behaviors。在这个文件夹中,创建一个MarkdownBehavior类:
  1. <?php
  2. namespace app\behaviors;
  3. use yii\base\Behavior;
  4. use yii\base\Event;
  5. use yii\base\InvalidConfigException;
  6. use yii\db\ActiveRecord;
  7. use yii\helpers\Markdown;
  8. class MarkdownBehavior extends Behavior
  9. {
  10. public $sourceAttribute;
  11. public $targetAttribute;
  12. public function init()
  13. {
  14. if (empty($this->sourceAttribute) ||
  15. empty($this->targetAttribute)) {
  16. throw new InvalidConfigException('Source and target must be set.');
  17. }
  18. parent::init();
  19. }
  20. public function events()
  21. {
  22. return [
  23. ActiveRecord::EVENT_BEFORE_INSERT => 'onBeforeSave',
  24. ActiveRecord::EVENT_BEFORE_UPDATE => 'onBeforeSave',
  25. ];
  26. }
  27. public function onBeforeSave(Event $event)
  28. {
  29. if
  30. ($this->owner->isAttributeChanged($this->sourceAttribute)) {
  31. $this->processContent();
  32. }
  33. }
  34. private function processContent()
  35. {
  36. $model = $this->owner;
  37. $source = $model->{$this->sourceAttribute};
  38. $model->{$this->targetAttribute} =
  39. Markdown::process($source);
  40. }
  41. }
  1. 附加行为到Post模型上:
  1. class Post extends ActiveRecord
  2. {
  3. ...
  4. public function behaviors()
  5. {
  6. return [
  7. 'markdown' => [
  8. 'class' => MarkdownBehavior::className(),
  9. 'sourceAttribute' => 'content_markdown',
  10. 'targetAttribute' => 'content_html',
  11. ],
  12. ];
  13. }
  14. }
  1. 运行测试并确保通过:
  1. Codeception PHP Testing Framework v2.0.9
  2. Powered by PHPUnit 4.8.27 by Sebastian Bergmann and
  3. contributors.
  4. Unit Tests (2)
  5. ----------------------------------------------------------------
  6. -----------
  7. Trying to test ...
  8. MarkdownBehaviorTest::testNewModelSave Ok
  9. Trying to test ...
  10. MarkdownBehaviorTest::testExistingModelSave Ok
  11. ----------------------------------------------------------------
  12. -----------
  13. Time: 329 ms, Memory: 17.00MB
  1. 完成了。我们已经创建了一个可复用的行为,并可以使用它用于所有未来的项目中,只需要将它连接到一个模型上。

工作原理…

首先看下测试用例。因为我们希望使用模型集,我们定义了fixtures。每次测试方法被执行的时候,一个fixture集合被放到了数据库中。

我们准备单元测试用以说明行为是如何工作的:

  • 首先,我们测试一个新的模型内容的处理。这个行为会将source属性中的markdown格式的文本,转换为HTML,并存储在target属性中。
  • 第二,我们对更新已有模型的内容进行测试。在修改了markdown内容以后,保存这个模型,我们可以得到更新后的HTML内容。

现在,我们转到有趣的实现细节上。在行为中,我们可以添加我们自己的方法,它将会被混合到附带有行为的模型中。此外,我们可以订阅拥有者的组件事件。我们使用它添加一个自己的监听:

  1. public function events()
  2. {
  3. return [
  4. ActiveRecord::EVENT_BEFORE_INSERT => 'onBeforeSave',
  5. ActiveRecord::EVENT_BEFORE_UPDATE => 'onBeforeSave',
  6. ];
  7. }

现在,我们可以实现这个监听器:

  1. public function onBeforeSave(Event $event)
  2. {
  3. if ($this->owner->isAttributeChanged($this->sourceAttribute))
  4. {
  5. $this->processContent();
  6. }
  7. }

在所有的方法中,我们可以使用owner属性来获取附带有行为的对象。一般情况下,我们可以附加任何行为到我们的模型、控制器、应用,以及其它继承了yii\base\Component类的组件。此外,我们可以重复附加一个行为到模型上,用以处理不同的属性:

  1. class Post extends ActiveRecord
  2. {
  3. ...
  4. public function behaviors()
  5. {
  6. return [
  7. [
  8. 'class' => MarkdownBehavior::className(),
  9. 'sourceAttribute' => 'description_markdown',
  10. 'targetAttribute' => 'description_html',
  11. ],
  12. [
  13. 'class' => MarkdownBehavior::className(),
  14. 'sourceAttribute' => 'content_markdown',
  15. 'targetAttribute' => 'content_html',
  16. ],
  17. ];
  18. }
  19. }

此外,我们可以像yii\behaviors\TimestampBehavior继承yii\base\AttributeBehavior,用以为任何事件更新指定的属性。

参考

为了了解更多关于行为和事件,参考如下页面:

欲了解更多关于markdown语法的信息,参考http://daringfireball.net/projects/markdown/

此外,参考本章中的制作可发布的扩展小节。