vistart/yii2-models

View on GitHub
traits/BlameableTrait.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php

/**
 *  _   __ __ _____ _____ ___  ____  _____
 * | | / // // ___//_  _//   ||  __||_   _|
 * | |/ // /(__  )  / / / /| || |     | |
 * |___//_//____/  /_/ /_/ |_||_|     |_|
 * @link https://vistart.name/
 * @copyright Copyright (c) 2016 vistart
 * @license https://vistart.name/license/
 */

namespace vistart\Models\traits;

use vistart\Models\queries\BaseUserQuery;
use yii\base\InvalidParamException;
use yii\base\ModelEvent;
use yii\base\NotSupportedException;
use yii\behaviors\BlameableBehavior;
use yii\caching\TagDependency;
use yii\data\Pagination;

/**
 * This trait is used for building blameable model. It contains following features:
 * 1.单列内容;多列内容待定;
 * 2.内容类型;具体类型应当自定义;
 * 3.内容规则;自动生成;
 * 4.归属用户 GUID;
 * 5.创建用户 GUID;
 * 6.上次更新用户 GUID;
 * 7.Confirmation features, provided by [[ConfirmationTrait]];
 * @property-read array $blameableAttributeRules Get all rules associated with
 * blameable.
 * @property array $blameableRules Get or set all the rules associated with
 * creator, updater, content and its ID, as well as all the inherited rules.
 * @property array $blameableBehaviors Get or set all the behaviors assoriated
 * with creator and updater, as well as all the inherited behaviors.
 * @property-read array $descriptionRules Get description property rules.
 * @property-read mixed $content Content.
 * @property-read boolean $contentCanBeEdited Whether this content could be edited.
 * @property-read array $contentRules Get content rules.
 * @property-read BaseUserModel $user
 * @property-read BaseUserModel $updater
 * @version 2.0
 * @author vistart <i@vistart.name>
 */
trait BlameableTrait
{
    use ConfirmationTrait,
        SelfBlameableTrait;

    private $blameableLocalRules = [];
    private $blameableLocalBehaviors = [];

    /**
     * @var boolean|string|array Specify the attribute(s) name of content(s). If
     * there is only one content attribute, you can assign its name. Or there
     * is multiple attributes associated with contents, you can assign their
     * names in array. If you don't want to use this feature, please assign
     * false.
     * For example:
     * ```php
     * public $contentAttribute = 'comment'; // only one field named as 'comment'.
     * ```
     * or
     * ```php
     * public $contentAttribute = ['year', 'month', 'day']; // multiple fields.
     * ```
     * or
     * ```php
     * public $contentAttribute = false; // no need of this feature.
     * ```
     * If you don't need this feature, you should add rules corresponding with
     * `content` in `rules()` method of your user model by yourself.
     */
    public $contentAttribute = 'content';

    /**
     * @var array built-in validator name or validatation method name and
     * additional parameters.
     */
    public $contentAttributeRule = ['string', 'max' => 255];

    /**
     * @var boolean|string Specify the field which stores the type of content.
     */
    public $contentTypeAttribute = false;

    /**
     * @var boolean|array Specify the logic type of content, not data type. If
     * your content doesn't need this feature. please specify false. If the
     * $contentAttribute is specified to false, this attribute will be skipped.
     * ```php
     * public $contentTypes = [
     *     'public',
     *     'private',
     *     'friend',
     * ];
     * ```
     */
    public $contentTypes = false;

    /**
     * @var boolean|string This attribute speicfy the name of description
     * attribute. If this attribute is assigned to false, this feature will be
     * skipped.
     */
    public $descriptionAttribute = false;

    /**
     * @var string
     */
    public $initDescription = '';

    /**
     * @var string the attribute that will receive current user ID value. This
     * attribute must be assigned.
     */
    public $createdByAttribute = "user_guid";

    /**
     * @var string the attribute that will receive current user ID value.
     * Set this property to false if you do not want to record the updater ID.
     */
    public $updatedByAttribute = "user_guid";

    /**
     * @var boolean Add combinated unique rule if assigned to true.
     */
    public $idCreatorCombinatedUnique = true;

    /**
     * @var boolean|string The name of user class which own the current entity.
     * If this attribute is assigned to false, this feature will be skipped, and
     * when you use create() method of UserTrait, it will be assigned with
     * current user class.
     */
    public $userClass;
    public static $cacheKeyBlameableRules = 'blameable_rules';
    public static $cacheTagBlameableRules = 'tag_blameable_rules';
    public static $cacheKeyBlameableBehaviors = 'blameable_behaviors';
    public static $cacheTagBlameableBehaviors = 'tag_blameable_behaviors';

    /**
     * @inheritdoc
     * ------------
     * The classical rules is like following:
     * [
     *     ['guid', 'required'],
     *     ['guid', 'unique'],
     *     ['guid', 'string', 'max' => 36],
     * 
     *     ['id', 'required'],
     *     ['id', 'unique'],
     *     ['id', 'string', 'max' => 4],
     * 
     *     ['create_time', 'safe'],
     *     ['update_time', 'safe'],
     * 
     *     ['ip_type', 'in', 'range' => [4, 6]],
     *     ['ip_1', 'number', 'integerOnly' => true, 'min' => 0],
     *     ['ip_2', 'number', 'integerOnly' => true, 'min' => 0],
     *     ['ip_3', 'number', 'integerOnly' => true, 'min' => 0],
     *     ['ip_4', 'number', 'integerOnly' => true, 'min' => 0],
     * 
     * 
     * ]
     * @return array
     */
    public function rules()
    {
        return $this->getBlameableRules();
    }

    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return $this->getBlameableBehaviors();
    }

    /**
     * Get total of contents which owned by their owner.
     * @return integer
     */
    public function countOfOwner()
    {
        $createdByAttribute = $this->createdByAttribute;
        return static::find()->where([$createdByAttribute => $this->$createdByAttribute])->count();
    }

    /**
     * Get content.
     * @return mixed
     */
    public function getContent()
    {
        $contentAttribute = $this->contentAttribute;
        if ($contentAttribute === false) {
            return null;
        }
        if (is_array($contentAttribute)) {
            $content = [];
            foreach ($contentAttribute as $key => $value) {
                $content[$key] = $this->$value;
            }
            return $content;
        }
        return $this->$contentAttribute;
    }

    /**
     * Set content.
     * @param mixed $content
     */
    public function setContent($content)
    {
        $contentAttribute = $this->contentAttribute;
        if ($contentAttribute === false) {
            return;
        }
        if (is_array($contentAttribute)) {
            foreach ($contentAttribute as $key => $value) {
                $this->$value = $content[$key];
            }
            return;
        }
        $this->$contentAttribute = $content;
    }

    /**
     * Determines whether content could be edited. Your should implement this
     * method by yourself.
     * @return boolean
     * @throws NotSupportedException
     */
    public function getContentCanBeEdited()
    {
        if ($this->contentAttribute === false) {
            return false;
        }
        throw new NotSupportedException("This method is not implemented.");
    }

    /**
     * Check it has been ever edited.
     * @return boolean Whether this content has ever been edited.
     */
    public function hasEverEdited()
    {
        $createdAtAttribute = $this->createdByAttribute;
        $updatedAtAttribute = $this->updatedByAttribute;
        if (!$createdAtAttribute || !$updatedAtAttribute) {
            return false;
        }
        return $this->$createdAtAttribute === $this->$updatedAtAttribute;
    }

    /**
     * Get blameable rules cache key.
     * @return string cache key.
     */
    public function getBlameableRulesCacheKey()
    {
        return static::className() . $this->cachePrefix . static::$cacheKeyBlameableRules;
    }

    /**
     * Get blameable rules cache tag.
     * @return string cache tag
     */
    public function getBlameableRulesCacheTag()
    {
        return static::className() . $this->cachePrefix . static::$cacheTagBlameableRules;
    }

    /**
     * Get the rules associated with content to be blamed.
     * @return array rules.
     */
    public function getBlameableRules()
    {
        $cache = $this->getCache();
        if ($cache) {
            $this->blameableLocalRules = $cache->get($this->getBlameableRulesCacheKey());
        }
        // 若当前规则不为空,且是数组,则认为是规则数组,直接返回。
        if (!empty($this->blameableLocalRules) && is_array($this->blameableLocalRules)) {
            return $this->blameableLocalRules;
        }

        // 父类规则与确认规则合并。
        if ($cache) {
            TagDependency::invalidate($cache, [$this->getEntityRulesCacheTag()]);
        }
        $rules = array_merge(
            parent::rules(),
            $this->getConfirmationRules(),
            $this->getBlameableAttributeRules(),
            $this->getDescriptionRules(),
            $this->getContentRules(),
            $this->getSelfBlameableRules()
        );
        $this->setBlameableRules($rules);
        return $this->blameableLocalRules;
    }

    /**
     * Get the rules associated with `createdByAttribute`, `updatedByAttribute`
     * and `idAttribute`-`createdByAttribute` combination unique.
     * @return array rules.
     */
    public function getBlameableAttributeRules()
    {
        $rules = [];
        // 创建者和上次修改者由 BlameableBehavior 负责,因此标记为安全。
        if (!is_string($this->createdByAttribute) || empty($this->createdByAttribute)) {
            throw new NotSupportedException('You must assign the creator.');
        }
        $rules[] = [
            [$this->createdByAttribute],
            'safe',
        ];

        if (is_string($this->updatedByAttribute) && !empty($this->updatedByAttribute)) {
            $rules[] = [
                [$this->updatedByAttribute],
                'safe',
            ];
        }

        if ($this->idCreatorCombinatedUnique && is_string($this->idAttribute)) {
            $rules ['id'] = [
                [$this->idAttribute,
                    $this->createdByAttribute],
                'unique',
                'targetAttribute' => [$this->idAttribute,
                    $this->createdByAttribute],
            ];
        }
        return $rules;
    }

    /**
     * Get the rules associated with `description` attribute.
     * @return array rules.
     */
    public function getDescriptionRules()
    {
        $rules = [];
        if (is_string($this->descriptionAttribute) && !empty($this->descriptionAttribute)) {
            $rules[] = [
                [$this->descriptionAttribute],
                'string'
            ];
            $rules[] = [
                [$this->descriptionAttribute],
                'default',
                'value' => $this->initDescription,
            ];
        }
        return $rules;
    }

    /**
     * Get the rules associated with `content` and `contentType` attributes.
     * @return array rules.
     */
    public function getContentRules()
    {
        if (!$this->contentAttribute) {
            return [];
        }
        $rules = [];
        $rules[] = [$this->contentAttribute, 'required'];
        if ($this->contentAttributeRule) {
            if (is_string($this->contentAttributeRule)) {
                $this->contentAttributeRule = [$this->contentAttributeRule];
            }
            if (is_array($this->contentAttributeRule)) {
                $rules[] = array_merge([$this->contentAttribute], $this->contentAttributeRule);
            }
        }

        if (!$this->contentTypeAttribute) {
            return $rules;
        }

        if (is_array($this->contentTypes) && !empty($this->contentTypes)) {
            $rules[] = [[
                $this->contentTypeAttribute],
                'required'];
            $rules[] = [[
                $this->contentTypeAttribute],
                'in',
                'range' => array_keys($this->contentTypes)];
        }
        return $rules;
    }

    /**
     * Set blameable rules.
     * @param array $rules
     */
    protected function setBlameableRules($rules = [])
    {
        $this->blameableLocalRules = $rules;
        $cache = $this->getCache();
        if ($cache) {
            $tagDependency = new TagDependency(['tags' => [$this->getBlameableRulesCacheTag()]]);
            $cache->set($this->getBlameableRulesCacheKey(), $rules, 0, $tagDependency);
        }
    }

    /**
     * Get blameable behaviors cache key.
     * @return string cache key.
     */
    public function getBlameableBehaviorsCacheKey()
    {
        return static::className() . $this->cachePrefix . static::$cacheKeyBlameableBehaviors;
    }

    /**
     * Get blameable behaviors cache tag.
     * @return string cache tag.
     */
    public function getBlameableBehaviorsCacheTag()
    {
        return static::className() . $this->cachePrefix . static::$cacheTagBlameableBehaviors;
    }

    /**
     * Get blameable behaviors. If current behaviors array is empty, the init
     * array will be given.
     * @return array
     */
    public function getBlameableBehaviors()
    {
        $cache = $this->getCache();
        if ($cache) {
            $this->blameableLocalBehaviors = $cache->get($this->getBlameableBehaviorsCacheKey());
        }
        if (empty($this->blameableLocalBehaviors) || !is_array($this->blameableLocalBehaviors)) {
            if ($cache) {
                TagDependency::invalidate($cache, [$this->getEntityBehaviorsCacheTag()]);
            }
            $behaviors = parent::behaviors();
            $behaviors['blameable'] = [
                'class' => BlameableBehavior::className(),
                'createdByAttribute' => $this->createdByAttribute,
                'updatedByAttribute' => $this->updatedByAttribute,
                'value' => [$this,
                    'onGetCurrentUserGuid'],
            ];
            $this->setBlameableBehaviors($behaviors);
        }
        return $this->blameableLocalBehaviors;
    }

    /**
     * Set blameable behaviors.
     * @param array $behaviors
     */
    protected function setBlameableBehaviors($behaviors = [])
    {
        $this->blameableLocalBehaviors = $behaviors;
        $cache = $this->getCache();
        if ($cache) {
            $tagDependencyConfig = ['tags' => [$this->getBlameableBehaviorsCacheTag()]];
            $tagDependency = new TagDependency($tagDependencyConfig);
            $cache->set($this->getBlameableBehaviorsCacheKey(), $behaviors, 0, $tagDependency);
        }
    }

    /**
     * Set description.
     * @return string description.
     */
    public function getDescription()
    {
        $descAttribute = $this->descriptionAttribute;
        return is_string($descAttribute) ? $this->$descAttribute : null;
    }

    /**
     * Get description.
     * @param string $desc description.
     * @return string|null description if enabled, or null if disabled.
     */
    public function setDescription($desc)
    {
        $descAttribute = $this->descriptionAttribute;
        return is_string($descAttribute) ? $this->$descAttribute = $desc : null;
    }

    /**
     * Get blame who owned this blameable model.
     * NOTICE! This method will not check whether `$userClass` exists. You should
     * specify it in `init()` method.
     * @return BaseUserQuery user.
     */
    public function getUser()
    {
        $userClass = $this->userClass;
        $model = $userClass::buildNoInitModel();
        return $this->hasOne($userClass::className(), [$model->guidAttribute => $this->createdByAttribute]);
    }

    /**
     * Get updater who updated this blameable model recently.
     * NOTICE! This method will not check whether `$userClass` exists. You should
     * specify it in `init()` method.
     * @return BaseUserQuery user.
     */
    public function getUpdater()
    {
        if (!is_string($this->updatedByAttribute)) {
            return null;
        }
        $userClass = $this->userClass;
        $model = $userClass::buildNoInitModel();
        return $this->hasOne($userClass::className(), [$model->guidAttribute => $this->updatedByAttribute]);
    }

    /**
     * This event is triggered before the model update.
     * This method is ONLY used for being triggered by event. DO NOT call,
     * override or modify it directly, unless you know the consequences.
     * @param ModelEvent $event
     */
    public function onContentChanged($event)
    {
        $sender = $event->sender;
        $sender->resetConfirmation();
    }

    /**
     * Return the current user's GUID if current model doesn't specify the owner
     * yet, or return the owner's GUID if current model has been specified.
     * This method is ONLY used for being triggered by event. DO NOT call,
     * override or modify it directly, unless you know the consequences.
     * @param ModelEvent $event
     * @return string the GUID of current user or the owner.
     */
    public function onGetCurrentUserGuid($event)
    {
        $sender = $event->sender;
        if (isset($sender->attributes[$sender->createdByAttribute])) {
            return $sender->attributes[$sender->createdByAttribute];
        }
        $identity = \Yii::$app->user->identity;
        if ($identity) {
            $igAttribute = $identity->guidAttribute;
            return $identity->$igAttribute;
        }
    }

    /**
     * Initialize type of content. the first of element[index is 0] of
     * $contentTypes will be used.
     * @param ModelEvent $event
     */
    public function onInitContentType($event)
    {
        $sender = $event->sender;
        if (!isset($sender->contentTypeAttribute) || !is_string($sender->contentTypeAttribute)) {
            return;
        }
        $contentTypeAttribute = $sender->contentTypeAttribute;
        if (!isset($sender->$contentTypeAttribute) &&
            !empty($sender->contentTypes) &&
            is_array($sender->contentTypes)) {
            $sender->$contentTypeAttribute = $sender->contentTypes[0];
        }
    }

    /**
     * Initialize description property with $initDescription.
     * @param ModelEvent $event
     */
    public function onInitDescription($event)
    {
        $sender = $event->sender;
        if (!isset($sender->descriptionAttribute) || !is_string($sender->descriptionAttribute)) {
            return;
        }
        $descriptionAttribute = $sender->descriptionAttribute;
        if (empty($sender->$descriptionAttribute)) {
            $sender->$descriptionAttribute = $sender->initDescription;
        }
    }

    /**
     * Attach events associated with blameable model.
     */
    public function initBlameableEvents()
    {
        $this->on(static::$eventConfirmationChanged, [$this, "onConfirmationChanged"]);
        $this->on(static::$eventNewRecordCreated, [$this, "onInitConfirmation"]);
        $contentTypeAttribute = $this->contentTypeAttribute;
        if (!isset($this->$contentTypeAttribute)) {
            $this->on(static::$eventNewRecordCreated, [$this, "onInitContentType"]);
        }
        $descriptionAttribute = $this->descriptionAttribute;
        if (!isset($this->$descriptionAttribute)) {
            $this->on(static::$eventNewRecordCreated, [$this, 'onInitDescription']);
        }
        $this->on(static::EVENT_BEFORE_UPDATE, [$this, "onContentChanged"]);
        $this->initSelfBlameableEvents();
    }

    /**
     * @inheritdoc
     */
    public function enabledFields()
    {
        $fields = parent::enabledFields();
        if (is_string($this->createdByAttribute)) {
            $fields[] = $this->createdByAttribute;
        }
        if (is_string($this->updatedByAttribute)) {
            $fields[] = $this->updatedByAttribute;
        }
        if (is_string($this->contentAttribute)) {
            $fields[] = $this->contentAttribute;
        }
        if (is_array($this->contentAttribute)) {
            $fields = array_merge($fields, $this->contentAttribute);
        }
        if (is_string($this->descriptionAttribute)) {
            $fields[] = $this->descriptionAttribute;
        }
        if (is_string($this->confirmationAttribute)) {
            $fields[] = $this->confirmationAttribute;
        }
        if (is_string($this->parentAttribute)) {
            $fields[] = $this->parentAttribute;
        }
        return $fields;
    }

    /**
     * Find all follows by specified identity. If `$identity` is null, the logged-in
     * identity will be taken.
     * @param string|integer $pageSize If it is 'all`, then will find all follows,
     * the `$currentPage` parameter will be skipped. If it is integer, it will be
     * regarded as sum of models in one page.
     * @param integer $currentPage The current page number, begun with 0.
     * @param {$this->userClass} $identity
     * @return static[] If no follows, null will be given, or return follow array.
     */
    public static function findAllByIdentityInBatch($pageSize = 'all', $currentPage = 0, $identity = null)
    {
        if ($pageSize === 'all') {
            return static::findByIdentity($identity)->all();
        }
        return static::findByIdentity($identity)->page($pageSize, $currentPage)->all();
    }

    /**
     * Find one follow by specified identity. If `$identity` is null, the logged-in
     * identity will be taken. If $identity doesn't has the follower, null will
     * be given.
     * @param integer $id user id.
     * @param boolean $throwException
     * @param {$this->userClass} $identity
     * @return static
     * @throws InvalidParamException
     */
    public static function findOneById($id, $throwException = true, $identity = null)
    {
        $query = static::findByIdentity($identity);
        if (!empty($id)) {
            $query = $query->id($id);
        }
        $model = $query->one();
        if (!$model && $throwException) {
            throw new InvalidParamException('Model Not Found.');
        }
        return $model;
    }

    /**
     * Get total of follows of specified identity.
     * @param {$this->userClass} $identity
     * @return integer total.
     */
    public static function countByIdentity($identity = null)
    {
        return static::findByIdentity($identity)->count();
    }

    /**
     * Get pagination, used for building contents page by page.
     * @param integer $limit
     * @param {$this->userClass} $identity
     * @return Pagination
     */
    public static function getPagination($limit = 10, $identity = null)
    {
        $limit = (int) $limit;
        $count = static::countByIdentity($identity);
        if ($limit > $count) {
            $limit = $count;
        }
        return new Pagination(['totalCount' => $count, 'pageSize' => $limit]);
    }
}