创建模型行为

现在的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:
<?php
use yii\db\Migration;
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}}');
    }
}
  1. 应用migration到工作和测试数据库上:
./yii migrate
tests/codeception/bin/yii migrate
  1. 创建Post模型:
<?php
namespace app\models;
use app\behaviors\MarkdownBehavior;
use yii\db\ActiveRecord;
/**
 * @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],
        ];
    }
}

如何做...

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

<?php
namespace app\tests\codeception\unit\fixtures;
use yii\test\ActiveFixture;
class PostFixture extends ActiveFixture
{
    public $modelClass = 'app\models\Post';
    public $dataFile = '@tests/codeception/unit/fixtures/data/post.php';
}
  1. 添加一个fixture数据到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>\n",
    ],
];
  1. 然后,我们需要创建一个测试用例,tests/codeception/unit/MarkdownBehaviorTest.php
<?php
namespace app\tests\codeception\unit;
use app\models\Post;
use app\tests\codeception\unit\fixtures\PostFixture;
use yii\codeception\DbTestCase;
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>\n", $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>\n", $post->content_html);
    }
    public function fixtures()
    {
        return [
            'posts' => [
                'class' => PostFixture::className(),
            ]
        ];
    }
}
  1. 运行单元测试:
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
  1. 现在我们需要实现行为,将它附加到模型上,并确保测试通过。创建一个新的文件夹behaviors。在这个文件夹中,创建一个MarkdownBehavior类:
<?php
namespace app\behaviors;
use yii\base\Behavior;
use yii\base\Event;
use yii\base\InvalidConfigException;
use yii\db\ActiveRecord;
use yii\helpers\Markdown;
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);
    }
}
  1. 附加行为到Post模型上:
class Post extends ActiveRecord
{
    ...
    public function behaviors()
    {
        return [
            'markdown' => [
                'class' => MarkdownBehavior::className(),
                'sourceAttribute' => 'content_markdown',
                'targetAttribute' => 'content_html',
            ],
        ];
    }
}
  1. 运行测试并确保通过:
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
  1. 完成了。我们已经创建了一个可复用的行为,并可以使用它用于所有未来的项目中,只需要将它连接到一个模型上。

工作原理...

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

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

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

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

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();
    }
}

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

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',
            ],
        ];
    }
}

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

参考

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

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

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

results matching ""

    No results matching ""