Creating model behaviors

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.

Getting ready

  1. Create a new 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.
  2. Create two databases for working and for tests.
  3. Configure Yii to use the first database in your primary application in config/db.php. Make sure the test application uses the second database in tests/codeception/config/config.php.
  4. Create a new migration:
    <?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}}');
        }
    }
  5. Apply the migration to both the working and test databases:
    ./yii migrate
    tests/codeception/bin/yii migrate
  6. Create the 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],
            ];
        }
    }

How to do it…

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';
}
  1. Add a fixture data file to 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>
    ",
        ],
    ];
  2. Then, we need to create a test case, 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(),
                ]
            ];
        }
    }
  3. Run the unit tests:
    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
    
  4. Now we need to implement behavior, attach it to the model, and make sure the test passes. Create a new directory, 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);
        }
    }
  5. Let's attach the behavior to the Post model:
    class Post extends ActiveRecord
    {
        ...
    
        public function behaviors()
        {
            return [
                'markdown' => [
                    'class' => MarkdownBehavior::className(),
                    'sourceAttribute' => 'content_markdown',
                    'targetAttribute' => 'content_html',
                ],
            ];
        }
    }
  6. Run the test and make sure it passes:
    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
    
  7. That's it. We've created a reusable behavior and can use it for all future projects by just connecting it to a model.

How it works…

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:

  • First, we are testing a processing of a new model content. The behavior must convert the Markdown text from the source attribute to HTML and store the second one to the target attribute.
  • Second, we are testing to update the content of the existing model. After changing the Markdown content and saving the model, we must get the updated HTML content.

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.

See also

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.165.246