<?php
namespace common\components\behaviors;
use Yii;
use yii\db\ActiveRecord;
use yii\base\Behavior;
use yii\validators\Validator;
use yii\web\View;
class LinkedListBehavior extends Behavior
{
const START = 'start';
const BEFORE = 'before';
const AFTER = 'after';
const END = 'end';
public $idAttributeName = 'id';
public $sortAttributeName = 'sort';
public $listIdAttributeName = 'list_id';
public $previousIdAttributeName = 'previous_id';
public $nextIdAttributeName = 'next_id';
public $sortAttributePrefix = null;
public $sortAttributePrefixFunction = null;
public $sortAttributeDelimiter = ':';
public $sortAttributeSize = 8;
public $sortAttributeReserveSymbols = 4;
public $position;
protected $length;
protected $ownerClassName;
protected $ownerTableName;
public function events()
{
return [
ActiveRecord::EVENT_BEFORE_DELETE => 'deleteCurrentElementOfList',
ActiveRecord::EVENT_AFTER_INSERT => 'insertCurrentElementOfList',
ActiveRecord::EVENT_BEFORE_UPDATE => 'deleteCurrentElementOfList',
ActiveRecord::EVENT_AFTER_UPDATE => 'insertCurrentElementOfList'
];
}
public function attach($owner){
parent::attach($owner);
$this->ownerClassName = $owner->className();
$this->ownerTableName = $owner->tableName();
$this->length = $this->getLengthOfCurrentList();
$this->registerLinkedListAttributeValidators($owner);
$this->registerLinkedListJsScripts();
}
public function attachConditions(&$query){
$query->addOrderBy([
$this->sortAttributeName => SORT_ASC
]);
}
protected function registerLinkedListAttributeValidators($owner){
$modelClass = strtolower(end(explode('\\', $this->ownerClassName)));
$owner->validators[] = Validator::createValidator('required', $owner, [
$this->listIdAttributeName,
'position'
]);
$owner->validators[] = Validator::createValidator('string', $owner, [
$this->sortAttributeName,
'position'
]);
$owner->validators[] = Validator::createValidator('integer', $owner, [
$this->listIdAttributeName,
$this->previousIdAttributeName,
$this->nextIdAttributeName
]);
$owner->validators[] = Validator::createValidator('required', $owner, [
$this->previousIdAttributeName
],
[
'when' => function($model){
return $model->position == self::AFTER;
},
'whenClient' => "function (attribute, value) {
return $(\"#{$modelClass}-position\").val() == '" . self::AFTER . "';
}",
]);
$owner->validators[] = Validator::createValidator('required', $owner, [
$this->nextIdAttributeName
],
[
'when' => function($model){
return $model->position === self::BEFORE;
},
'whenClient' => "function (attribute, value) {
return $(\"#{$modelClass}-position\").val() == '" . self::BEFORE . "';
}",
]);
}
protected function registerLinkedListJsScripts(){
$modelClass = strtolower(end(explode('\\', $this->ownerClassName)));
Yii::$app->controller->getView()->registerJS("
var switchInputs = function(){
$(\".field-{$modelClass}-{$this->previousIdAttributeName}\").hide();
$(\".field-{$modelClass}-{$this->nextIdAttributeName}\").hide();
if($(\"#{$modelClass}-position\").val() == '" . self::AFTER . "'){
$(\".field-{$modelClass}-{$this->previousIdAttributeName}\").show();
}
if($(\"#{$modelClass}-position\").val() == '" . self::BEFORE . "'){
$(\".field-{$modelClass}-{$this->nextIdAttributeName}\").show();
}
};
switchInputs();
$(\"#{$modelClass}-position\").change(switchInputs);
", View::POS_END);
}
public function getLengthOfCurrentList(){
$ownerClassName = $this->ownerClassName;
return $ownerClassName::find()->where([
$this->listIdAttributeName => $this->owner->{$this->listIdAttributeName}
])->count();
}
public function getFirstElementOfList(){
$ownerClassName = $this->ownerClassName;
$query = $ownerClassName::find()->where([
$this->listIdAttributeName => $this->owner->{$this->listIdAttributeName},
$this->previousIdAttributeName => null
]);
if(!$this->owner->isNewRecord){
$query->andWhere(['!=', $this->idAttributeName, $this->owner->{$this->idAttributeName}]);
}
return $query->one();
}
public function getLastElementOfList(){
$ownerClassName = $this->ownerClassName;
$query = $ownerClassName::find()->where([
$this->listIdAttributeName => $this->owner->{$this->listIdAttributeName},
$this->nextIdAttributeName => null
]);
if(!$this->owner->isNewRecord){
$query->andWhere(['!=', $this->idAttributeName, $this->owner->{$this->idAttributeName}]);
}
return $query->one();
}
public function getPreviousElementOfList(){
$ownerClassName = $this->ownerClassName;
if($this->owner->{$this->previousIdAttributeName} === null){
return null;
}
return $ownerClassName::find()->where([
$this->listIdAttributeName => $this->owner->{$this->listIdAttributeName},
$this->idAttributeName => $this->owner->{$this->previousIdAttributeName}
])->one();
}
public function getNextElementOfList(){
$ownerClassName = $this->ownerClassName;
if($this->owner->{$this->nextIdAttributeName} === null){
return null;
}
return $ownerClassName::find()->where([
$this->listIdAttributeName => $this->owner->{$this->listIdAttributeName},
$this->idAttributeName => $this->owner->{$this->nextIdAttributeName}
])->one();
}
public function insertCurrentElementOfList(){
if($this->owner->isNewRecord){
return;
}
$transaction = Yii::$app->db->beginTransaction();
try{
if($this->position === self::START){
$this->pushFrontCurrentElementOfList();
}
if($this->position === self::END){
$this->pushBackCurrentElementOfList();
}
if($this->position === self::BEFORE){
$this->pushBeforeCurrentElementOfList();
}
if($this->position === self::AFTER){
$this->pushAfterCurrentElementOfList();
}
$transaction->commit();
}catch(\Exception $e){
$transaction->rollBack();
}catch (\Throwable $e) {
$transaction->rollBack();
}
}
public function updateCurrentElementOfList(){
$this->deleteCurrentElementOfList();
$this->insertCurrentElementOfList();
}
public function deleteCurrentElementOfList(){
if($this->owner->isNewRecord){
return false;
}
$previous = $this->getPreviousElementOfList();
$next = $this->getNextElementOfList();
if($previous === null & $next === null){
return true;
}
$transaction = Yii::$app->db->beginTransaction();
try{
if($previous === null && $next !== null){
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->previousIdAttributeName => null,
),
array(
$this->idAttributeName => $next->{$this->idAttributeName}
))->execute();
}
if($previous !== null && $next === null){
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->nextIdAttributeName => null,
),
array(
$this->idAttributeName => $previous->{$this->idAttributeName}
))->execute();
}
if($previous !== null && $next !== null){
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->nextIdAttributeName => $next->{$this->idAttributeName}
),
array(
$this->idAttributeName => $previous->{$this->idAttributeName}
))->execute();
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->previousIdAttributeName => $previous->{$this->idAttributeName},
),
array(
$this->idAttributeName => $next->{$this->idAttributeName}
))->execute();
}
$transaction->commit();
return true;
}catch(\Exception $e){
$transaction->rollBack();
}catch (\Throwable $e) {
$transaction->rollBack();
}
return false;
}
protected function pushFrontCurrentElementOfList(){
$this->position = self::START;
$next = $this->getFirstElementOfList();
if($next === null){
$next_id = null;
}else{
$next_id = $next->{$this->idAttributeName};
}
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->previousIdAttributeName => null,
$this->nextIdAttributeName => $next_id,
$this->sortAttributeName => $this->calculateSortAttribute(),
),
array(
$this->idAttributeName => $this->owner->{$this->idAttributeName}
))->execute();
if($next === null){
return;
}
// update getNextElementOfList item
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->previousIdAttributeName => $this->owner->{$this->idAttributeName}
),
array(
$this->idAttributeName => $next->{$this->idAttributeName}
))->execute();
}
protected function pushBackCurrentElementOfList(){
$this->position = self::END;
$previous = $this->getLastElementOfList();
if($previous === null){
$previous_id = null;
}else{
$previous_id = $previous->{$this->idAttributeName};
}
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->previousIdAttributeName => $previous_id,
$this->nextIdAttributeName => null,
$this->sortAttributeName => $this->calculateSortAttribute(),
),
array(
$this->idAttributeName => $this->owner->{$this->idAttributeName}
))->execute();
if($previous === null){
return;
}
// update getPreviousElementOfList item
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->nextIdAttributeName => $this->owner->{$this->idAttributeName}
),
array(
$this->idAttributeName => $previous->{$this->idAttributeName}
))->execute();
}
protected function pushAfterCurrentElementOfList(){
$this->position = self::AFTER;
$previous = $this->getPreviousElementOfList();
if($previous === null){
$this->pushFrontCurrentElementOfList();
return;
}
if($previous->{$this->nextIdAttributeName} === null){
$this->pushBackCurrentElementOfList();
return;
}
$next = $previous->getNextElementOfList();
if($next === null){
$this->pushBackCurrentElementOfList();
return;
}
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->previousIdAttributeName => $previous->{$this->idAttributeName},
$this->nextIdAttributeName => $next->{$this->idAttributeName},
$this->sortAttributeName => $this->calculateSortAttribute(),
),
array(
$this->idAttributeName => $this->owner->{$this->idAttributeName}
))->execute();
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->nextIdAttributeName => $this->owner->{$this->idAttributeName}
),
array(
$this->idAttributeName => $previous->{$this->idAttributeName}
))->execute();
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->previousIdAttributeName => $this->owner->{$this->idAttributeName}
),
array(
$this->idAttributeName => $next->{$this->idAttributeName}
))->execute();
}
protected function pushBeforeCurrentElementOfList(){
$this->position = self::BEFORE;
$next = $this->getNextElementOfList();
if($next === null){
$this->pushBackCurrentElementOfList();
return;
}
if($next->{$this->previousIdAttributeName} === null){
$this->pushFrontCurrentElementOfList();
return;
}
$previous = $next->getPreviousElementOfList();
if($previous === null){
$this->pushFrontCurrentElementOfList();
return;
}
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->previousIdAttributeName => $previous->{$this->idAttributeName},
$this->nextIdAttributeName => $next->{$this->idAttributeName},
$this->sortAttributeName => $this->calculateSortAttribute(),
),
array(
$this->idAttributeName => $this->owner->{$this->idAttributeName}
))->execute();
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->nextIdAttributeName => $this->owner->{$this->idAttributeName}
),
array(
$this->idAttributeName => $previous->{$this->idAttributeName}
))->execute();
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->previousIdAttributeName => $this->owner->{$this->idAttributeName}
),
array(
$this->idAttributeName => $next->{$this->idAttributeName}
))->execute();
}
protected function encodeSortAttribute($position){
$result = str_pad($position, $this->sortAttributeSize + $this->sortAttributeReserveSymbols - 1 ,'0',STR_PAD_LEFT);
if(is_string($this->sortAttributePrefix)){
$result = implode($this->sortAttributeDelimiter, array(
$this->sortAttributePrefix,
$result,
));
}
if(!is_string($this->sortAttributePrefix) && is_string($this->sortAttributePrefixFunction)){
if($this->owner->hasMethod($this->sortAttributePrefixFunction)){
$result = implode($this->sortAttributeDelimiter, array(
$this->owner->{$this->sortAttributePrefixFunction}(),
$result,
));
}
}
return $result;
}
protected function decodeSortAttribute($code){
$result = $code;
if(is_string($this->sortAttributePrefix) || (!is_string($this->sortAttributePrefix) && is_string($this->sortAttributePrefixFunction))){
$result = explode($this->sortAttributeDelimiter, $code);
if(sizeof($result) == 1){
$result = $result[0];
}else if(sizeof($result) > 1){
$result = $result[sizeof($result) - 1];
}
}
return ltrim($result, '0');
}
public function calculateSortAttribute(){
if($this->position == self::START){
$next = $this->getFirstElementOfList();
if($next === null){
return $this->encodeSortAttribute(10 ** ($this->sortAttributeReserveSymbols - 1));
}
return $this->encodeSortAttribute(
$this->calculateAverageSortAttribute(0, $this->decodeSortAttribute($next->{$this->sortAttributeName}))
);
}
if($this->position == self::END){
$last = $this->getLastElementOfList();
$number = floor($this->decodeSortAttribute($last->{$this->sortAttributeName}) / (10 ** ($this->sortAttributeReserveSymbols - 1)) + 1);
return $this->encodeSortAttribute(
$number * (10 ** ($this->sortAttributeReserveSymbols - 1))
);
}
if($this->position == self::AFTER){
$previous = $this->getPreviousElementOfList();
if($previous === null){
$this->position = self::START;
$this->calculateSortAttribute();
}
if($previous->{$this->nextIdAttributeName} === null){
$this->position = self::END;
$this->calculateSortAttribute();
}
$next = $previous->getNextElementOfList();
if($next === null){
$this->position = self::END;
$this->calculateSortAttribute();
}
return $this->encodeSortAttribute(
$this->calculateAverageSortAttribute(
$this->decodeSortAttribute($previous->{$this->sortAttributeName}),
$this->decodeSortAttribute($next->{$this->sortAttributeName})
)
);
}
if($this->position == self::BEFORE){
$next = $this->getNextElementOfList();
if($next === null){
$this->position = self::END;
$this->calculateSortAttribute();
}
if($next->{$this->previousIdAttributeName} === null){
$this->position = self::START;
$this->calculateSortAttribute();
}
$previous = $next->getPreviousElementOfList();
if($previous === null){
$this->position = self::START;
$this->calculateSortAttribute();
}
return $this->encodeSortAttribute(
$this->calculateAverageSortAttribute(
$this->decodeSortAttribute($previous->{$this->sortAttributeName}),
$this->decodeSortAttribute($next->{$this->sortAttributeName})
)
);
}
}
protected function calculateAverageSortAttribute($previous, $next){
$position = ceil(($previous + $next) / 2);
if($position > $previous && $position < $next){
return $position;
}
if($this->updateSortAttributeInAllRecords()){
return $this->calculateSortAttribute();
}
}
protected function updateSortAttributeInAllRecords(){
$ownerClassName = $this->ownerClassName;
$items = $ownerClassName::find()->where([
$this->listIdAttributeName => $this->owner->{$this->listIdAttributeName}
])->andWhere([
'not', [$this->sortAttributeName => null]
])->orderBy([
$this->sortAttributeName => SORT_ASC
]);
$i = 1;
$transaction = Yii::$app->db->beginTransaction();
try{
foreach ($items->each(100) as $item){
Yii::$app->db->createCommand()->update($this->ownerTableName, array(
$this->sortAttributeName => $this->encodeSortAttribute($i * (10 ** ($this->sortAttributeReserveSymbols - 1)))
),
array(
$this->idAttributeName => $item->{$this->idAttributeName}
))->execute();
$i++;
}
$transaction->commit();
return true;
}catch(\Exception $e){
$transaction->rollBack();
}catch (\Throwable $e) {
$transaction->rollBack();
}
return false;
}
}