3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PhalconAdvent Calendar 2014

Day 11

ビヘイビアとは - フィールドを GZIP 圧縮して永続化する方法

Last updated at Posted at 2014-12-10

こんにちは。実務で Phalcon を使っています。
今回は Model にペタッとアタッチして使うビヘイビアについて書いてみます。

ビヘイビアとはなにか

Phalcon\Mvc\Model は onConstruct や beforeSave, afterSave といったイベントをハンドルすることができます。永続化が実行される直前、直後でなにかを実行したい場合に便利です。

これらのイベントハンドラを使うことで、たいていのことはできます。
例えばレコードの作成日時と更新日時を記録するには以下のように書きます。

class BlogEntry extends \Phalcon\Mvc\Model {
    
    public $created_at;
    public $updated_at;

    public function beforeCreate()
    {
        $this->created_at = date('r');
        $this->updated_at = date('r');
    }

    public function beforeUpdate()
    {
        $this->updated_at = date('r');
    }
    
}

さらに、あるフィールドを GZIP 圧縮するようにしてみましょう。
圧縮されたフィールドは MySQL であれば BLOB 型カラムに格納できます。

class BlogEntry extends \Phalcon\Mvc\Model {
    
    public $created_at;
    public $updated_at;
    public $body;
    public $metadata;

    public function beforeCreate()
    {
        $this->created_at = date('r');
    }

    public function beforeUpdate()
    {
        $this->updated_at = date('r');
    }
    
    public function beforeSave() {
    	$this->body = gzcompress($this->body);
    	$this->metadata = gzcompress($this->metadata);
    }
    
    public function afterFetch() {
    	$this->body = gzuncompress($this->body);
    	$this->metadata = gzuncompress($this->metadata);
    }
    
}

あ、これだと保存した直後は圧縮されたままになってしまいますね。

$entry = new BlogEntry();
$entry->body = "あばばばばばぼぼぼぼぼぼぼぼ";
$entry->save();
echo $entry->body; // ->文字化け

afterSave を追加すれば解決します。

class BlogEntry extends \Phalcon\Mvc\Model {
    
    public $created_at;
    public $updated_at;
    public $body;
    public $metadata;

    public function beforeCreate()
    {
        $this->created_at = date('r');
    }

    public function beforeUpdate()
    {
        $this->updated_at = date('r');
    }
    
    public function beforeSave() {
    	$this->body = gzcompress($this->body);
    	$this->metadata = gzcompress($this->metadata);
    }
    
    public function afterSave() { // 追加!(だけど内容は afterFetch と同じ...
    	$this->body = gzuncompress($this->body);
    	$this->metadata = gzuncompress($this->metadata);
    }
    
    public function afterFetch() {
    	$this->body = gzuncompress($this->body);
    	$this->metadata = gzuncompress($this->metadata);
    }
    
}

お前は何が言いたいんだ早く言え!

イベントシステムの問題は、メソッドがその処理の目的を表していないためいちいち「このモデルは~のタイミングで何をしでかすんだ?」とコードを読む必要があります。それに、読むべきコードは各イベントメソッドに散在してしまうため見通しがすこぶる悪くなります。

こういった「状態に影響する」ものは副作用です、副作用をパッと見て限定できなくなればバグの名産地となることは間違いありません。

ビヘイビア is 何

こういったイベントによる振る舞いを、目的ごとに定義したものがビヘイビアです。
例えば最初の作成日時を自動的に設定するビヘイビアはPhalcon が用意してくれています

モデルの initialize メソッドにて addBehavior するだけでOKです。

use Phalcon\Mvc\Model\Behavior\Timestampable;

class BlogEntry extends \Phalcon\Mvc\Model {

	public $created_at;
	public $updated_at;
	public $body;
	public $metadata;

	public function initialize()
	{
		$this->addBehavior(new Timestampable(
			array(
				'beforeCreate' => array(
					'field' => array(
						'created_at',
						'updated_at'
					),
					'format' => 'Y-m-d'
				),
				'beforeUpdate' => array(
					'field' => 'updated_at',
					'format' => 'Y-m-d'
				),
			)
		));
	}

}

GZIP 圧縮するビヘイビアを独自に定義する

フィールドを圧縮するビヘイビアを作ってみました。

GzCompressBehavior
use Phalcon\Mvc\Model\Behavior,
	Phalcon\Mvc\Model\BehaviorInterface;

/**
 * フィールドを gzip 圧縮して永続化するビヘイビア
 */
class GzCompressBehavior extends Behavior implements BehaviorInterface {

	/**
	 * @type array|\string[]
	 */
	private $fields;
	private $level = -1;
	
	/**
	 * @param string[] $fields 圧縮するフィールド名を格納した配列
	 * @param integer $level 圧縮レベル デフォルトで zlib ライブラリのデフォルト値
	 * @param array $options
	 */
	public function __construct(array $fields, $level = -1, $options = null)
	{
		parent::__construct($options);
		$this->fields = $fields;
	}

	/**
	 * @param string $type
	 * @param \Phalcon\Mvc\Model $model
	 */
	public function notify($type, $model)
	{
		switch ($type) {
			case 'beforeSave':
				foreach ($this->fields as $field) {
					$value = $this->getValue($model, $field);
					$this->setValue($model, $field, \gzcompress($value, $this->level));
				}
				break;
			case 'afterSave':
			case 'afterFetch':
				foreach ($this->fields as $field) {
					$value = $this->getValue($model, $field);
					$this->setValue($model, $field, \gzuncompress($value));
				}
				break;
		}
	}

	/**
	 * @param \App\Models\ModelBase $model
	 * @param string $field
	 * @return mixed
	 * @throws \ErrorException
	 */
	private function getValue($model, $field)
	{
		$getter = "get" . $field->toPascalCase();
		if (method_exists($model, $getter)) {
			return $model->$getter();
		} else if (property_exists($model, $field)) {
			return $model->$field;
		} else {
			throw new \ErrorException("field $field not found on class " . get_class($model));
		}
	}

	/**
	 * @param \App\Models\ModelBase $model
	 * @param string $field
	 * @param mixed $value
	 * @throws \ErrorException
	 */
	private function setValue($model, $field, $value)
	{
		$setter = "set" . $field->toPascalCase();
		if (method_exists($model, $setter)) {
			$model->$setter($value);
		} else if (property_exists($model, $field)) {
			$model->$field = $value;
		} else {
			throw new \ErrorException("field $field not found on class " . get_class($model));
		}
	}

	private function toPascalCase($str, $delim = '_')
	{
		$str = strtolower($str);
		$str = str_replace($delim, ' ', $str);
		$str = ucwords($str);
		$str = str_replace(' ', '', $str);
		return $str;
	}

}

これを addBehavior して完成です!

use Phalcon\Mvc\Model\Behavior\Timestampable;

class BlogEntry extends \Phalcon\Mvc\Model {

	public $created_at;
	public $updated_at;
	public $body;
	public $metadata;

	public function initialize()
	{
		$this->addBehavior(new Timestampable(
			array(
				'beforeCreate' => array(
					'field' => array(
						'created_at',
						'updated_at'
					),
					'format' => 'Y-m-d'
				),
				'beforeUpdate' => array(
					'field' => 'updated_at',
					'format' => 'Y-m-d'
				),
			)
		));
		// 圧縮したいフィールド名を配列で渡す
		$this->addBehavior(new GzCompressBehavior(array(
			'body', 'metadata'
		)));
	}

}

これでもし振る舞いに不具合があった場合、対象のビヘイビアを修正すればそれを利用しているすべての Model に反映されます。それにパッと見ただけで Model の振る舞いと意図が読めるようになりました。やはりコードは目的ごとに書くべきです。

3
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?