luyadev/luya-module-cms

View on GitHub
src/models/NavItemPageBlockItem.php

Summary

Maintainability
B
4 hrs
Test Coverage
D
62%
<?php

namespace luya\cms\models;

use luya\cms\admin\Module;
use luya\helpers\ArrayHelper;
use luya\traits\CacheableTrait;
use Yii;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;
use yii\helpers\Json;

/**
 * Represents an ITEM for the type NavItemPage.
 *
 * Sort_index numbers always starts from 0 and not from 1, like a default array behaviour. If a
 * negative sort_index is provided its always the last sort_index item (reason: we dont know the sort key of
 * the "at the end" dropparea).
 *
 * @property integer $id
 * @property integer $block_id
 * @property string $placeholder_var
 * @property integer $nav_item_page_id
 * @property integer $prev_id
 * @property string $json_config_values
 * @property string $json_config_cfg_values
 * @property integer $is_dirty
 * @property integer $create_user_id
 * @property integer $update_user_id
 * @property integer $timestamp_create
 * @property integer $timestamp_update
 * @property integer $sort_index
 * @property integer $is_hidden
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.0
 */
class NavItemPageBlockItem extends ActiveRecord
{
    use CacheableTrait;
    private array $_olds = [];

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'cms_nav_item_page_block_item';
    }

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        $this->on(self::EVENT_BEFORE_DELETE, [$this, 'eventBeforeDelete']);
        $this->on(self::EVENT_AFTER_INSERT, [$this, 'eventAfterInsert']);
        $this->on(self::EVENT_AFTER_UPDATE, [$this, 'eventAfterUpdate']);
        $this->on(self::EVENT_AFTER_DELETE, [$this, 'eventAfterDelete']);
        $this->on(self::EVENT_AFTER_VALIDATE, [$this, 'ensureInputValues']);
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['json_config_values', 'json_config_cfg_values'], function ($attribute, $params) {
                // if its not an array, the attribute is not dirty and has not to be serialized from input.
                if (is_array($this->$attribute)) {
                    $data = ArrayHelper::typeCast($this->$attribute);
                    foreach ($data as $key => $value) {
                        if ($value === null) {
                            unset($data[$key]);
                        }
                    }

                    if (isset($data['__e']) && count($data) >= 2) {
                        unset($data['__e']);
                    }

                    // placeholder in order to make sure an object will be unserailized instead of an array.
                    if (empty($data)) {
                        $data['__e'] = '__v';
                    }

                    $this->$attribute = Json::encode($data, JSON_FORCE_OBJECT);
                }
            }, 'skipOnEmpty' => false],
            [['block_id', 'nav_item_page_id', 'prev_id', 'create_user_id', 'update_user_id', 'timestamp_create', 'timestamp_update', 'sort_index'], 'integer'],
            [['is_dirty', 'is_hidden'], 'boolean'],
            [['placeholder_var'], 'required'],
            [['json_config_values', 'json_config_cfg_values'], 'string'],
            [['placeholder_var'], 'string', 'max' => 80],
            [['variation'], 'safe'],
        ];
    }

    /**
     * @inheritdoc
     */
    public function scenarios()
    {
        $scene = parent::scenarios();
        $scene['restcreate'] = $scene['default'];
        $scene['restupdate'] = $scene['default'];
        return $scene;
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'block_id' => 'Block ID',
            'placeholder_var' => 'Placeholder Var',
            'nav_item_page_id' => 'Nav Item Page ID',
            'prev_id' => 'Prev ID',
            'json_config_values' => 'Json Config Values',
            'json_config_cfg_values' => 'Json Config Cfg Values',
            'is_dirty' => 'Is Dirty',
            'create_user_id' => 'Create User ID',
            'update_user_id' => 'Update User ID',
            'timestamp_create' => 'Timestamp Create',
            'timestamp_update' => 'Timestamp Update',
            'sort_index' => 'Sort Index',
            'is_hidden' => 'Is Hidden',
            'variation' => 'Variation',
        ];
    }

    /**
     * @inheritdoc
     */
    public function fields()
    {
        $fields = parent::fields();
        $fields['objectdetail'] = fn ($model) => NavItemPage::getBlockItem($model, $model->navItemPage);
        return $fields;
    }

    /**
     *
     * @param \yii\base\Event $event
     */
    protected function ensureInputValues($event)
    {
        // sort index fixture

        if (!$this->isNewRecord) {
            $this->_olds = $this->getOldAttributes();

            // ensure circular reference is not possible when updating
            if ($this->id == $this->prev_id) {
                return $this->addError('prev_id', 'Circular references are not allowed. Its not possible to drag a block into its own placeholder.');
            }
        }
        // its a negative value, so its a last item, lets find the last index for current config
        if ($this->sort_index < 0) {
            $last = self::originalFind()->andWhere(['nav_item_page_id' => $this->nav_item_page_id, 'placeholder_var' => $this->placeholder_var, 'prev_id' => $this->prev_id])->orderBy(['sort_index' => SORT_DESC])->one();
            if (!$last) {
                $this->sort_index = 0;
            } else {
                $this->sort_index = $last->sort_index + 1;
            }
        } else { // its not a negative value, we have to find the positions after the current sort index and update to a higher level
            $higher = self::originalFind()->where(['>=', 'sort_index', $this->sort_index])->andWhere(['nav_item_page_id' => $this->nav_item_page_id, 'placeholder_var' => $this->placeholder_var, 'prev_id' => $this->prev_id])->all();
            foreach ($higher as $item) {
                $newSortIndex = $item->sort_index + 1;
                Yii::$app->db->createCommand()->update(self::tableName(), ['sort_index' => $newSortIndex], ['id' => $item->id])->execute();
            }
        }

        // manipulate timestamps
        if ($this->isNewRecord) {
            $this->timestamp_create = time();
            $this->timestamp_update = time();
            $this->create_user_id = Module::getAuthorUserId();
        } else {
            $this->deleteHasCache(['blockcache', (int) $this->id]);
            $this->deleteHasCache(['blockcache', (int) $this->prev_id]);
            $this->is_dirty = true;
            $this->update_user_id = Module::getAuthorUserId();
            $this->timestamp_update = time();
        }
    }

    /**
     * Event after update
     */
    public function eventAfterUpdate($event)
    {
        $this->updateNavItemTimesamp();
        if (!empty($this->_olds)) {

            $oldPlaceholderVar = $this->_olds['placeholder_var'] ?? false;
            $oldPrevId = isset($this->_olds['prev_id']) ? (int) $this->_olds['prev_id'] : 0;

            if ($oldPlaceholderVar != $this->placeholder_var || $oldPrevId != $this->prev_id) {
                $this->reindex($this->nav_item_page_id, $oldPlaceholderVar, $oldPrevId);
            }
            $this->reindex($this->nav_item_page_id, $this->placeholder_var, $this->prev_id);

            Log::addAfterSave(2, [
                'tableName' => 'cms_nav_item_page_block_item',
                'action' => 'update',
                'row' => $this->id,
                'pageTitle' => $this->droppedPageTitle,
                'blockName' => $this->getBlockNameForLog()
            ], $event);
        }
    }

    /**
     * Ensures the block name
     *
     * @return string
     * @since 4.1.0
     */
    private function getBlockNameForLog()
    {
        return $this->block ? $this->block->getNameForLog() : '[class has been removed from the filesystem]';
    }

    /**
     * Event before delete
     */
    public function eventBeforeDelete()
    {
        // delete all attached sub blocks
        $this->deleteAllSubBlocks($this->id);
        //save block data for afterDeleteEvent
        $this->_olds = $this->getOldAttributes();
        // log event
        Log::add(3, [
            'tableName' => 'cms_nav_item_page_block_item',
            'action' => 'delete',
            'row' => $this->id,
            'pageTitle' => $this->droppedPageTitle,
            'blockName' => $this->getBlockNameForLog()
        ], 'cms_nav_item_page_block_item', $this->id);
    }

    /**
     * Event after delete
     */
    public function eventAfterDelete()
    {
        $this->updateNavItemTimesamp();
        if (!empty($this->_olds)) {
            $this->reindex($this->_olds['nav_item_page_id'] ?? 0, $this->_olds['placeholder_var'] ?? null, $this->_olds['prev_id'] ?? 0);
        }
    }

    /**
     * Event after insert
     */
    public function eventAfterInsert($event)
    {
        if ($this->id == $this->prev_id) {
            // ensurer no infinte requests can happen.
            // https://github.com/luyadev/luya-module-cms/issues/106
            $this->updateAttributes(['prev_id' => 0]);
        }

        $this->updateNavItemTimesamp();
        $this->reindex($this->nav_item_page_id, $this->placeholder_var, $this->prev_id);

        Log::addAfterSave(1, [
            'tableName' => 'cms_nav_item_page_block_item',
            'action' => 'insert',
            'row' => $this->id,
            'pageTitle' => $this->droppedPageTitle,
            'blockName' => $this->getBlockNameForLog()
        ], $event);
    }

    /**
     *
     * @param unknown $blockId
     */
    private function deleteAllSubBlocks($blockId)
    {
        if ($blockId) {
            $subBlocks = NavItemPageBlockItem::findAll(['prev_id' => $blockId]);
            foreach ($subBlocks as $block) {
                // check for attached sub blocks and start recursion
                $attachedBlocks = NavItemPageBlockItem::findAll(['prev_id' => $block->id]);
                if ($attachedBlocks) {
                    $this->deleteAllSubBlocks($block->id);
                }
                $block->delete();
            }
        }
    }

    /**
     * Reindex the page block items in order to get requestd sorting.
     *
     * @param string|int|null $navItemPageId
     * @param string|null $placeholderVar
     * @param string|int|null $prevId
     */
    private function reindex($navItemPageId, $placeholderVar, $prevId)
    {
        $index = 0;
        $datas = self::originalFind()->andWhere(['nav_item_page_id' => $navItemPageId, 'placeholder_var' => $placeholderVar, 'prev_id' => $prevId])->orderBy(['sort_index' => SORT_ASC, 'timestamp_create' => SORT_DESC])->all();
        foreach ($datas as $item) {
            Yii::$app->db->createCommand()->update(self::tableName(), ['sort_index' => $index], ['id' => $item->id])->execute();
            ++$index;
        }
    }

    private function updateNavItemTimesamp()
    {
        // if state makes sure this does not happend when the nav item page is getting deleted and triggers the child delete process.
        if ($this->navItemPage) {
            if ($this->navItemPage->forceNavItem) {
                $this->navItemPage->forceNavItem->updateTimestamp();
            }
        }
    }

    public function getDroppedPageTitle()
    {
        // if state makes sure this does not happend when the nav item page is getting deleted and triggers the child delete process.
        if ($this->navItemPage) {
            if ($this->navItemPage->forceNavItem) {
                return $this->navItemPage->forceNavItem->title;
            }
        }

        return;
    }

    public static function originalFind()
    {
        return Yii::createObject(ActiveQuery::class, [static::class]);
    }

    /**
     * Default sort on find command.
     *
     * @return ActiveQuery
     */
    public static function find()
    {
        return parent::find()->orderBy(['sort_index' => SORT_ASC]);
    }

    /**
     * Get the block for the page block item
     *
     * @return ActiveQuery
     */
    public function getBlock()
    {
        return $this->hasOne(Block::class, ['id' => 'block_id']);
    }

    /**
     * Get the corresponding page where the block is stored.
     *
     * @return ActiveQuery
     */
    public function getNavItemPage()
    {
        return $this->hasOne(NavItemPage::class, ['id' => 'nav_item_page_id']);
    }
}