traits/UserRelationTrait.php
<?php
/**
* _ __ __ _____ _____ ___ ____ _____
* | | / // // ___//_ _// || __||_ _|
* | |/ // /(__ ) / / / /| || | | |
* |___//_//____/ /_/ /_/ |_||_| |_|
* @link https://vistart.name/
* @copyright Copyright (c) 2016 vistart
* @license https://vistart.name/license/
*/
namespace vistart\Models\traits;
use vistart\Models\models\BaseUserModel;
use vistart\Models\traits\MultipleBlameableTrait as mb;
use yii\base\ModelEvent;
/**
* Relation features.
* This trait should be used in user relation model which is extended from
* [[BaseBlameableModel]], and is specified `$userClass` property. And the user
* class should be extended from [[BaseUserModel]], or any other classes used
* [[UserTrait]].
* Notice: Several methods associated with "inserting", "updating" and "removing" may
* involve more DB operations, I strongly recommend those methods to be placed in
* transaction execution, in order to ensure data consistency.
* If you want to use group feature, the class used [[UserRelationGroupTrait]]
* must be used coordinately.
* @property array $groupGuids the guid array of all groups which owned by current relation.
* @property-read array $allGroups
* @property-read array $nonGroupMembers
* @property-read integer $groupsCount
* @property-read array $groupsRules
* @property boolean $isFavorite
* @property-read BaseUserModel $initiator
* @property-read BaseUserModel $recipient
* @property-read static $opposite
* @version 2.0
* @author vistart <i@vistart.name>
*/
trait UserRelationTrait
{
use mb,
MutualTrait {
mb::addBlame as addGroup;
mb::createBlame as createGroup;
mb::addOrCreateBlame as addOrCreateGroup;
mb::removeBlame as removeGroup;
mb::removeAllBlames as removeAllGroups;
mb::getBlame as getGroup;
mb::getOrCreateBlame as getOrCreateGroup;
mb::getBlameds as getGroupMembers;
mb::getBlameGuids as getGroupGuids;
mb::setBlameGuids as setGroupGuids;
mb::getAllBlames as getAllGroups;
mb::getNonBlameds as getNonGroupMembers;
mb::getBlamesCount as getGroupsCount;
mb::getMultipleBlameableAttributeRules as getGroupsRules;
mb::getEmptyBlamesJson as getEmptyGroupJson;
}
/**
* @var string
*/
public $remarkAttribute = 'remark';
public static $relationSingle = 0;
public static $relationMutual = 1;
public $relationType = 1;
public $relationTypes = [
0 => 'Single',
1 => 'Mutual',
];
/**
* @var string the attribute name of which determines the relation type.
*/
public $mutualTypeAttribute = 'type';
public static $mutualTypeNormal = 0x00;
public static $mutualTypeSuspend = 0x01;
/**
* @var array Mutual types.
*/
public static $mutualTypes = [
0x00 => 'Normal',
0x01 => 'Suspend',
];
/**
* @var string the attribute name of which determines the `favorite` field.
*/
public $favoriteAttribute = 'favorite';
/**
* Permit to build self relation.
* @var boolean
*/
public $relationSelf = false;
/**
* Get whether this relation is favorite or not.
* @return boolean
*/
public function getIsFavorite()
{
$favoriteAttribute = $this->favoriteAttribute;
return is_string($favoriteAttribute) ? (int) $this->$favoriteAttribute > 0 : null;
}
/**
* Set favorite.
* @param boolean $fav
*/
public function setIsFavorite($fav)
{
$favoriteAttribute = $this->favoriteAttribute;
return is_string($favoriteAttribute) ? $this->$favoriteAttribute = ($fav ? 1 : 0) : null;
}
/**
* @inheritdoc
*/
public function rules()
{
return array_merge(parent::rules(), $this->getUserRelationRules());
}
/**
* Validation rules associated with user relation.
* @return array rules.
*/
public function getUserRelationRules()
{
$rules = [];
if ($this->relationType == static::$relationMutual) {
$rules = [
[[$this->mutualTypeAttribute], 'in', 'range' => array_keys(static::$mutualTypes)],
[[$this->mutualTypeAttribute], 'default', 'value' => static::$mutualTypeNormal],
];
}
return array_merge($rules, $this->getRemarkRules(), $this->getFavoriteRules(), $this->getGroupsRules(), $this->getOtherGuidRules());
}
/**
* Get remark.
* @return string remark.
*/
public function getRemark()
{
$remarkAttribute = $this->remarkAttribute;
return is_string($remarkAttribute) ? $this->$remarkAttribute : null;
}
/**
* Set remark.
* @param string $remark
* @return string remark.
*/
public function setRemark($remark)
{
$remarkAttribute = $this->remarkAttribute;
return is_string($remarkAttribute) ? $this->$remarkAttribute = $remark : null;
}
/**
* Validation rules associated with remark attribute.
* @return array rules.
*/
public function getRemarkRules()
{
return is_string($this->remarkAttribute) ? [
[[$this->remarkAttribute], 'string'],
[[$this->remarkAttribute], 'default', 'value' => ''],
] : [];
}
/**
* Validation rules associated with favorites attribute.
* @return array rules.
*/
public function getFavoriteRules()
{
return is_string($this->favoriteAttribute) ? [
[[$this->favoriteAttribute], 'boolean'],
[[$this->favoriteAttribute], 'default', 'value' => 0],
] : [];
}
/**
* Validation rules associated with other guid attribute.
* @return array rules.
*/
public function getOtherGuidRules()
{
$rules = array_merge($this->getMutualRules(), [
[[$this->otherGuidAttribute, $this->createdByAttribute], 'unique', 'targetAttribute' => [$this->otherGuidAttribute, $this->createdByAttribute]],
]);
return $rules;
}
/**
* Attach events associated with user relation.
*/
public function initUserRelationEvents()
{
$this->on(static::EVENT_INIT, [$this, 'onInitBlamesLimit']);
$this->on(static::$eventNewRecordCreated, [$this, 'onInitGroups']);
$this->on(static::$eventNewRecordCreated, [$this, 'onInitRemark']);
$this->on(static::$eventMultipleBlamesChanged, [$this, 'onBlamesChanged']);
$this->on(static::EVENT_AFTER_INSERT, [$this, 'onInsertRelation']);
$this->on(static::EVENT_AFTER_UPDATE, [$this, 'onUpdateRelation']);
$this->on(static::EVENT_AFTER_DELETE, [$this, 'onDeleteRelation']);
}
/**
* Get opposite relation against self.
* @return static
*/
public function getOpposite()
{
if ($this->isNewRecord) {
return null;
}
$createdByAttribute = $this->createdByAttribute;
$otherGuidAttribute = $this->otherGuidAttribute;
return static::find()->opposite($this->$createdByAttribute, $this->$otherGuidAttribute);
}
/**
* Check whether the initiator is followed by recipient.
* @param BaseUserModel $initiator
* @param BaseUserModel $recipient
* @return boolean
*/
public static function isFollowed($initiator, $recipient)
{
return ((int) static::find()->initiators($recipient)->recipients($initiator)->count()) > 0;
}
/**
* Check whether the initiator is following recipient.
* @param BaseUserModel $initiator
* @param BaseUserModel $recipient
* @return boolean
*/
public static function isFollowing($initiator, $recipient)
{
return ((int) static::find()->initiators($initiator)->recipients($recipient)->count()) > 0;
}
/**
* Check whether the initiator is following and followed by recipient mutually (Single Relation).
* Or check whether the initiator and recipient are friend whatever the mutual type is normal or suspend.
* @param BaseUserModel $initiator
* @param BaseUserModel $recipient
* @return boolean
*/
public static function isMutual($initiator, $recipient)
{
return static::isFollowed($initiator, $recipient) && static::isFollowing($initiator, $recipient);
}
/**
* Check whether the initiator is following and followed by recipient mutually (Single Relation).
* Or check whether the initiator and recipient are friend if the mutual type is normal.
* @param BaseUserModel $initiator
* @param BaseUserModel $recipient
* @return boolean
*/
public static function isFriend($initiator, $recipient)
{
$query = static::find();
$model = $query->noInitModel;
/* @var $model static */
if ($model->relationType == static::$relationSingle) {
return static::isMutual($initiator, $recipient);
}
if ($model->relationType == static::$relationMutual) {
$relation = (int) static::find()->initiators($initiator)->recipients($recipient)->andWhere([$model->mutualTypeAttribute => static::$mutualTypeNormal])->count();
$inverse = (int) static::find()->recipients($initiator)->initiators($recipient)->andWhere([$model->mutualTypeAttribute => static::$mutualTypeNormal])->count();
return $relation && $inverse;
}
return false;
}
/**
* Build new or return existed suspend mutual relation, of return null if
* current type is not mutual.
* @see buildRelation()
* @param BaseUserModel|string $user Initiator or its GUID.
* @param BaseUserModel|string $other Recipient or its GUID.
* @return static The relation will be
* given if exists, or return a new relation.
*/
public static function buildSuspendRelation($user, $other)
{
$relation = static::buildRelation($user, $other);
if ($relation->relationType != static::$relationMutual) {
return null;
}
$btAttribute = $relation->mutualTypeAttribute;
$relation->$btAttribute = static::$mutualTypeSuspend;
return $relation;
}
/**
* Build new or return existed normal relation.
* The status of mutual relation will be changed to normal if it is not.
* @see buildRelation()
* @param BaseUserModel|string $user Initiator or its GUID.
* @param BaseUserModel|string $other Recipient or its GUID.
* @return static The relation will be
* given if exists, or return a new relation.
*/
public static function buildNormalRelation($user, $other)
{
$relation = static::buildRelation($user, $other);
if ($relation->relationType == static::$relationMutual) {
$btAttribute = $relation->mutualTypeAttribute;
$relation->$btAttribute = static::$mutualTypeNormal;
}
return $relation;
}
/**
* Build new or return existed relation between initiator and recipient.
* If relation between initiator and recipient is not found, new relation will
* be built. If initiator and recipient are the same one and it is not allowed
* to build self relation, null will be given.
* If you want to know whether the relation exists, you can check the return
* value of `getIsNewRecord()` method.
* @param BaseUserModel|string $user Initiator or its GUID.
* @param BaseUserModel|string $other Recipient or its GUID.
* @return static The relation will be
* given if exists, or return a new relation. Or return null if not allowed
* to build self relation,
*/
protected static function buildRelation($user, $other)
{
$relationQuery = static::find()->initiators($user)->recipients($other);
$noInit = $relationQuery->noInitModel;
$relation = $relationQuery->one();
if (!$relation) {
$createdByAttribute = $noInit->createdByAttribute;
$otherGuidAttribute = $noInit->otherGuidAttribute;
$userClass = $noInit->userClass;
if ($user instanceof BaseUserModel) {
$userClass = $userClass ? : $user->className();
$user = $user->guid;
}
if ($other instanceof BaseUserModel) {
$other = $other->guid;
}
if (!$noInit->relationSelf && $user == $other) {
return null;
}
$relation = new static([$createdByAttribute => $user, $otherGuidAttribute => $other, 'userClass' => $userClass]);
}
return $relation;
}
/**
* Build opposite relation throughout the current relation. The opposite
* relation will be given if existed.
* @param static $relation
* @return static
*/
protected static function buildOppositeRelation($relation)
{
if (!$relation) {
return null;
}
$createdByAttribute = $relation->createdByAttribute;
$otherGuidAttribute = $relation->otherGuidAttribute;
$opposite = static::buildRelation($relation->$otherGuidAttribute, $relation->$createdByAttribute);
if ($relation->relationType == static::$relationSingle) {
$opposite->relationType = static::$relationSingle;
} elseif ($relation->relationType == static::$relationMutual) {
$mutualTypeAttribute = $relation->mutualTypeAttribute;
$opposite->$mutualTypeAttribute = $relation->$mutualTypeAttribute;
}
return $opposite;
}
/**
* Remove myself.
* @return integer|false The number of relations removed, or false if the remove
* is unsuccessful for some reason. Note that it is possible the number of relations
* removed is 0, even though the remove execution is successful.
*/
public function remove()
{
return $this->delete();
}
/**
* Remove first relation between initiator(s) and recipient(s).
* @param BaseUserModel|string|array $user Initiator or its guid, or array of them.
* @param BaseUserModel|string|array $other Recipient or its guid, or array of them.
* @return integer|false The number of relations removed.
*/
public static function removeOneRelation($user, $other)
{
return static::find()->initiators($user)->recipients($other)->one()->delete();
}
/**
* Remove all relations between initiator(s) and recipient(s).
* @param BaseUserModel|string|array $user Initiator or its guid, or array of them.
* @param BaseUserModel|string|array $other Recipient or its guid, or array of them.
* @return integer The number of relations removed.
*/
public static function removeAllRelations($user, $other)
{
$rni = static::buildNoInitModel();
$createdByAttribute = $rni->createdByAttribute;
$otherGuidAttribute = $rni->otherGuidAttribute;
return static::deleteAll([$createdByAttribute => $user, $otherGuidAttribute => $other]);
}
/**
* Get first relation between initiator(s) and recipient(s).
* @param BaseUserModel|string|array $user Initiator or its guid, or array of them.
* @param BaseUserModel|string|array $other Recipient or its guid, or array of them.
* @return static
*/
public static function findOneRelation($user, $other)
{
return static::find()->initiators($user)->recipients($other)->one();
}
/**
* Get first opposite relation between initiator(s) and recipient(s).
* @param BaseUserModel|string $user Initiator or its guid, or array of them.
* @param BaseUserModel|string $other Recipient or its guid, or array of them.
* @return static
*/
public static function findOneOppositeRelation($user, $other)
{
return static::find()->initiators($other)->recipients($user)->one();
}
/**
* Get user's or users' all relations, or by specified groups.
* @param BaseUserModel|string|array $user Initiator or its GUID, or Initiators or their GUIDs.
* @param BaseUserRelationGroupModel|string|array|null $groups UserRelationGroup
* or its guid, or array of them. If you do not want to delimit the groups, please assign null.
* @return array all eligible relations
*/
public static function findOnesAllRelations($user, $groups = null)
{
return static::find()->initiators($user)->groups($groups)->all();
}
/**
* Initialize groups attribute.
* @param ModelEvent $event
*/
public function onInitGroups($event)
{
$sender = $event->sender;
$sender->removeAllGroups();
}
/**
* Initialize remark attribute.
* @param ModelEvent $event
*/
public function onInitRemark($event)
{
$sender = $event->sender;
$remarkAttribute = $sender->remarkAttribute;
is_string($remarkAttribute) ? $sender->$remarkAttribute = '' : null;
}
/**
* The event triggered after insert new relation.
* The opposite relation should be inserted without triggering events
* simultaneously after new relation inserted,
* @param ModelEvent $event
*/
public function onInsertRelation($event)
{
$sender = $event->sender;
if ($sender->relationType == static::$relationMutual) {
$opposite = static::buildOppositeRelation($sender);
$opposite->off(static::EVENT_AFTER_INSERT, [$opposite, 'onInsertRelation']);
if (!$opposite->save()) {
$opposite->recordWarnings();
}
$opposite->on(static::EVENT_AFTER_INSERT, [$opposite, 'onInsertRelation']);
}
}
/**
* The event triggered after update relation.
* The opposite relation should be updated without triggering events
* simultaneously after existed relation removed.
* @param ModelEvent $event
*/
public function onUpdateRelation($event)
{
$sender = $event->sender;
if ($sender->relationType == static::$relationMutual) {
$opposite = static::buildOppositeRelation($sender);
$opposite->off(static::EVENT_AFTER_UPDATE, [$opposite, 'onUpdateRelation']);
if (!$opposite->save()) {
$opposite->recordWarnings();
}
$opposite->on(static::EVENT_AFTER_UPDATE, [$opposite, 'onUpdateRelation']);
}
}
/**
* The event triggered after delete relation.
* The opposite relation should be deleted without triggering events
* simultaneously after existed relation removed.
* @param ModelEvent $event
*/
public function onDeleteRelation($event)
{
$sender = $event->sender;
if ($sender->relationType == static::$relationMutual) {
$createdByAttribute = $sender->createdByAttribute;
$otherGuidAttribute = $sender->otherGuidAttribute;
$sender->off(static::EVENT_AFTER_DELETE, [$sender, 'onDeleteRelation']);
static::removeAllRelations($sender->$otherGuidAttribute, $sender->$createdByAttribute);
$sender->on(static::EVENT_AFTER_DELETE, [$sender, 'onDeleteRelation']);
}
}
}