luyadev/luya-module-cms

View on GitHub
src/models/NavItem.php

Summary

Maintainability
D
1 day
Test Coverage
F
45%
<?php

namespace luya\cms\models;

use luya\admin\base\GenericSearchInterface;
use luya\admin\helpers\Angular;
use luya\admin\models\Lang;
use luya\admin\models\User;
use luya\admin\ngrest\base\NgRestActiveQuery;
use luya\cms\admin\Module;
use luya\helpers\Inflector;
use Yii;
use yii\base\Exception;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;

/**
 * NavItem Model represents a Item bound to Nav and Language, each Nav(Menu) can contain a nav_item for each language.Each
 * cms_nav_item is related to a type of item (module, page or redirect) which is stored in nav_item_type (number) and another field
 * nav_item_type_id (pk of the table).
 *
 * @property \luya\cms\models\NavItemPage|\luya\cms\models\NavItemModule\luya\cms\models\NavItemRedirect $type The type object based on the current type id
 * @property integer $id
 * @property integer $nav_id
 * @property integer $lang_id
 * @property integer $nav_item_type
 * @property integer $nav_item_type_id
 * @property integer $create_user_id
 * @property integer $update_user_id
 * @property integer $timestamp_create
 * @property integer $timestamp_update
 * @property string $title
 * @property string $alias
 * @property string $description
 * @property string $keywords
 * @property string $title_tag
 * @property integer $image_id
 * @property integer $is_url_strict_parsing_disabled
 * @property integer $is_cacheable
 *
 * @property \luya\cms\models\Nav $nav Nav Model.
 * @property \luya\admin\models\User $createUser
 * @property \luya\admin\models\User $updateUser
 * @property \luya\admin\models\Lang $lang
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.0
 */
class NavItem extends ActiveRecord implements GenericSearchInterface
{
    public const TYPE_PAGE = 1;

    public const TYPE_MODULE = 2;

    public const TYPE_REDIRECT = 3;

    public $parent_nav_id;

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        $this->on(self::EVENT_BEFORE_VALIDATE, [$this, 'validateAlias']);
        $this->on(self::EVENT_BEFORE_INSERT, [$this, 'beforeCreate']);
        $this->on(self::EVENT_BEFORE_UPDATE, [$this, 'eventBeforeUpdate']);

        $this->on(self::EVENT_BEFORE_DELETE, [$this, 'eventLogger']);
        $this->on(self::EVENT_AFTER_INSERT, [$this, 'eventLogger']);
        $this->on(self::EVENT_AFTER_UPDATE, [$this, 'eventLogger']);

        $this->on(self::EVENT_AFTER_DELETE, function ($event) {
            $type = $event->sender->getType();
            if ($type) {
                $type->delete();
            }

            foreach (NavItemPage::find()->where(['nav_item_id' => $event->sender->id])->all() as $version) {
                $version->delete();
            }
        });
    }

    /**
     * Log the current event in a database to retrieve data in case of emergency. This method will be trigger
     * on: EVENT_BEFORE_DELETE, EVENT_AFTER_INSERT & EVENT_AFTER_UPDATE
     *
     * @param \yii\base\Event $event
     */
    protected function eventLogger($event)
    {
        switch ($event->name) {
            case 'afterInsert':
                return Log::addAfterSave(Log::LOG_TYPE_INSERT, ['tableName' => 'cms_nav_item', 'action' => 'insert', 'row' => $this->id], $event);
            case 'afterUpdate':
                return Log::addAfterSave(Log::LOG_TYPE_UPDATE, ['tableName' => 'cms_nav_item', 'action' => 'update', 'row' => $this->id], $event);
            case 'beforeDelete':
            case 'afterDelete':
                return Log::add(Log::LOG_TYPE_DELETE, ['tableName' => 'cms_nav_item', 'action' => 'delete', 'row' => $this->id], 'cms_nav_item', $this->id, $this->toArray());
        }
    }

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

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            ['nav_item_type_id', 'required', 'isEmpty' => fn ($value) => empty($value), 'when' => fn () => !$this->isNewRecord],
            [['description', 'keywords'], 'string'],
            [['title'], 'string', 'max' => 180],
            [['alias'], 'string', 'max' => 180],
            [['title_tag'], 'string', 'max' => 255],
            [['lang_id', 'title', 'alias', 'nav_item_type'], 'required'],
            [['nav_id', 'lang_id', 'nav_item_type', 'nav_item_type_id', 'create_user_id', 'update_user_id', 'timestamp_create', 'timestamp_update', 'image_id', 'is_url_strict_parsing_disabled', 'is_cacheable'], 'integer'],
            [['alias'], 'match', 'pattern' => '/\_|\/|\\\/i', 'not' => true],
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'title' => Module::t('model_navitem_title_label'),
            'alias' => Module::t('model_navitem_alias_label'),
            'title_tag' => Module::t('model_navitem_title_tag_label'),
            'image_id' => Module::t('model_navitem_image_id_label'),
        ];
    }

    /**
     * Create User relation.
     *
     * @return NgRestActiveQuery|ActiveQuery
     */
    public function getCreateUser(): \luya\admin\ngrest\base\NgRestActiveQuery|\yii\db\ActiveQuery
    {
        return $this->hasOne(User::class, ['id' => 'create_user_id']);
    }

    /**
     * Update User relation.
     *
     * @return NgRestActiveQuery|ActiveQuery
     */
    public function getUpdateUser(): \luya\admin\ngrest\base\NgRestActiveQuery|\yii\db\ActiveQuery
    {
        return $this->hasOne(User::class, ['id' => 'update_user_id']);
    }

    /**
     * Slugify the current alias attribute.
     */
    public function slugifyAlias()
    {
        $this->alias = Inflector::slug($this->alias, '-', true, false);
    }

    private $_type;

    /**
     * GET the type object based on the nav_item_type defintion and the nav_item_type_id which is the
     * primary key for the corresponding type table (page, module, redirect). This approach has been choosen
     * do dynamically extend type of pages whithout any limitation.
     *
     * @return \luya\cms\models\NavItemPage|\luya\cms\models\NavItemModule|\luya\cms\models\NavItemRedirect|bool Returns the object based on the type
     * @throws Exception
     */
    public function getType(): \luya\cms\models\NavItemPage|\luya\cms\models\NavItemModule|\luya\cms\models\NavItemRedirect|bool
    {
        if ($this->_type === null) {
            // what kind of item type are we looking for
            if ($this->nav_item_type == self::TYPE_PAGE) {
                $this->_type = NavItemPage::findOne($this->nav_item_type_id);
            } elseif ($this->nav_item_type == self::TYPE_MODULE) {
                $this->_type = NavItemModule::findOne($this->nav_item_type_id);
            } elseif ($this->nav_item_type == self::TYPE_REDIRECT) {
                $this->_type = NavItemRedirect::findOne($this->nav_item_type_id);
            }

            if ($this->_type === null) {
                $this->_type = false;
            }

            // set context for the object
            /// 5.4.2016: Discontinue, as the type model does have getNavItem relation
            //$this->_type->setNavItem($this);
        }

        return $this->_type;
    }

    /**
     * Get the related nav entry for this nav_item.
     *
     * @return NgRestActiveQuery|ActiveQuery
     */
    public function getNav(): \luya\admin\ngrest\base\NgRestActiveQuery|\yii\db\ActiveQuery
    {
        return $this->hasOne(Nav::class, ['id' => 'nav_id']);
    }

    /**
     * Get the render content for the specific type, see the definition of `getContent()` in the available types.
     *
     * @return mixed
     */
    public function getContent()
    {
        return $this->getType()->getContent();
    }

    /**
     * Update attributes of the current nav item type relation.
     *
     * @return boolean Whether the update has been successfull or not
     */
    public function updateType(array $postData)
    {
        $model = $this->getType();
        $model->setAttributes($postData);
        return $model->update();
    }

    /**
     * Get the parent nav id information from the existing getNav relation and overrides the public properties parent_nav_id of this model.
     * This is applied because of the validation process to make sure this rewrite does not already exists.
     */
    public function setParentFromModel()
    {
        $this->parent_nav_id = $this->nav->parent_nav_id;
    }

    /**
     * Alias verification.
     *
     * @param string $alias
     * @param integer $langId
     */
    public function verifyAlias($alias, $langId)
    {
        if (Yii::$app->hasModule($alias) && $this->parent_nav_id == null) {
            $this->addError('alias', Module::t('nav_item_model_error_modulenameexists', ['alias' => $alias]));
            return false;
        }

        // when no parent nav id is given, the post value is `null` therefore we have the explicit set the value to `0`.
        $parentNavId = $this->parent_nav_id ?: 0;

        /**
         * Group by website_id
         * @since 4.0.0
         */
        $exists = static::find()
            ->leftJoin('cms_nav', 'cms_nav_item.nav_id=cms_nav.id')
            ->leftJoin('cms_nav_container', 'cms_nav.nav_container_id=cms_nav_container.id')
            ->where(['cms_nav_item.alias' => $alias, 'cms_nav_item.lang_id' => $langId, 'cms_nav.parent_nav_id' => $parentNavId])
            ->groupBy('cms_nav_container.website_id')
            ->exists();
        if ($exists) {
            $this->addError('alias', Module::t('nav_item_model_error_urlsegementexistsalready'));
            return false;
        }
    }

    /**
     * Alias Validator.
     */
    public function validateAlias()
    {
        $dirty = $this->getDirtyAttributes(['alias']);
        if (!isset($dirty['alias'])) {
            return true;
        }

        if (!$this->verifyAlias($this->alias, $this->lang_id)) {
            return false;
        }
    }

    /**
     * Before create event.
     */
    public function beforeCreate()
    {
        $this->timestamp_create = time();
        $this->timestamp_update = 0;
        $this->create_user_id = Module::getAuthorUserId();
        $this->update_user_id = Module::getAuthorUserId();
        $this->slugifyAlias();
    }

    /**
     * Before update event.
     */
    public function eventBeforeUpdate()
    {
        $this->timestamp_update = time();
        $this->update_user_id = Module::getAuthorUserId();
        $this->slugifyAlias();
    }

    /**
     * Udpate the current model timestamp and user.
     *
     * This is triggered from outside model as short cut.
     */
    public function updateTimestamp()
    {
        $this->updateAttributes([
            'timestamp_update' => time(),
            'update_user_id' => Module::getAuthorUserId(),
        ]);
    }

    public function fields()
    {
        $fields = parent::fields();
        $fields['is_cacheable'] = function ($model) {
            return (int) $model->is_cacheable;
            // return Angular::typeCast($model->is_cacheable); use for admin@4.0 release
        };
        $fields['is_url_strict_parsing_disabled'] = function ($model) {
            return (int) $model->is_url_strict_parsing_disabled;
            // return Angular::typeCast($model->is_url_strict_parsing_disabled); use for admin@4.0 release
        };
        return $fields;
    }

    /* GenericSearchInterface */

    /**
     * @inheritdoc
     */
    public function genericSearchFields()
    {
        return ['title', 'container'];
    }

    /**
     * @inheritdoc
     */
    public function genericSearchHiddenFields()
    {
        return ['nav_id'];
    }

    /**
     * @inheritdoc
     */
    public function genericSearch($searchQuery)
    {
        $data = [];

        foreach (self::find()->select(['nav_id', 'title'])->orWhere(['like', 'title', $searchQuery])->with('nav')->distinct()->each() as $item) {
            if ($item->nav) {
                $data[] = [
                    'title' => $item->title,
                    'nav_id' => $item->nav_id,
                    'container' => $item->nav->navContainer->name,
                ];
            }
        }

        return $data;
    }

    /**
     * @inheritdoc
     */
    public function genericSearchStateProvider()
    {
        return [
            'state' => 'custom.cmsedit',
            'params' => [
                'navId' => 'nav_id',
            ],
        ];
    }

    /**
     * Lang Active Query.
     *
     * @return NgRestActiveQuery|ActiveQuery
     */
    public function getLang(): \luya\admin\ngrest\base\NgRestActiveQuery|\yii\db\ActiveQuery
    {
        return $this->hasOne(Lang::class, ['id' => 'lang_id']);
    }

    /**
     *
     * Copy content of type cms_nav_item_page to a target nav item. This will create a new entry in cms_nav_item_page and for every used block a new entry in cms_nav_item_page_block_item
     *
     * @param NavItem $targetNavItem nav item object
     * @return bool
     */
    public function copyPageItem(NavItem $targetNavItem)
    {
        if ($this->nav_item_type !== self::TYPE_PAGE) {
            return false;
        }

        $sourcePageItem = NavItemPage::findOne($this->nav_item_type_id);

        if (!$sourcePageItem) {
            return false;
        }
        $pageItem = new NavItemPage();
        $pageItem->attributes = $sourcePageItem->toArray();
        $pageItem->nav_item_id = $targetNavItem->id;

        if (!$pageItem->save()) {
            return false;
        }

        $targetNavItem->nav_item_type_id = $pageItem->id;
        if (!$targetNavItem->save()) {
            return false;
        }

        $batch = NavItemPageBlockItem::find()
            ->where(['nav_item_page_id' => $sourcePageItem->id])
            ->asArray()
            ->batch();

        $idLink = [];
        foreach ($batch as $pageBlocks) {
            foreach ($pageBlocks as $block) {
                $blockItem = new NavItemPageBlockItem();
                $blockItem->attributes = $block;
                $blockItem->nav_item_page_id = $pageItem->id;
                if ($blockItem->save()) {
                    // store the old block id with the new block id
                    $idLink[$block['id']] = $blockItem->id;
                }

                unset($blockItem);
            }
        }
        // check if prev_id is used, check if id is in set - get new id and set new prev_ids in copied items
        $batch = NavItemPageBlockItem::find()
            ->select(['id', 'prev_id'])
            ->where(['nav_item_page_id' => $pageItem->id])
            ->asArray()
            ->batch();

        foreach ($batch as $newPageBlocks) {
            foreach ($newPageBlocks as $block) {
                $prevId = $block['prev_id'];
                if ($block['prev_id'] && isset($idLink[$prevId])) {
                    NavItemPageBlockItem::updateAll(['prev_id' => $idLink[$prevId]], ['id' => $block['id']]);
                }
            }
        }

        return true;
    }

    /**
     *
     * Copy content of type cms_nav_item_module to a target nav item. This will create a new entry in cms_nav_item_module.
     *
     * @param NavItem $targetNavItem
     * @return bool
     */
    public function copyModuleItem(NavItem $targetNavItem)
    {
        if ($this->nav_item_type !== self::TYPE_MODULE) {
            return false;
        }

        $sourceModuleItem = NavItemModule::findOne($this->nav_item_type_id);
        if (!$sourceModuleItem) {
            return false;
        }
        $moduleItem = new NavItemModule();
        $moduleItem->attributes = $sourceModuleItem->toArray();

        if (!$moduleItem->save()) {
            return false;
        }

        $targetNavItem->nav_item_type_id = $moduleItem->id;
        return $targetNavItem->save();
    }

    /**
     *
     * Copy content of type cms_nav_item_redirect to a target nav item. This will create a new entry in cms_nav_item_redirect.
     *
     * @param NavItem $targetNavItem
     * @return bool
     */
    public function copyRedirectItem(NavItem $targetNavItem)
    {
        if ($this->nav_item_type !== self::TYPE_REDIRECT) {
            return false;
        }

        $sourceRedirectItem = NavItemRedirect::findOne($this->nav_item_type_id);
        if (!$sourceRedirectItem) {
            return false;
        }
        $redirectItem = new NavItemRedirect();
        $redirectItem->attributes = $sourceRedirectItem->toArray();

        if (!$redirectItem->save()) {
            return false;
        }

        $targetNavItem->nav_item_type_id = $redirectItem->id;
        return $targetNavItem->save();
    }

    /**
     *
     * Copy nav item type content.
     *
     * @param NavItem $targetNavItem
     * @return bool
     * @throws Exception type not recognized (1,2,3)
     */
    public function copyTypeContent(NavItem $targetNavItem)
    {
        return match ($this->nav_item_type) {
            self::TYPE_PAGE => $this->copyPageItem($targetNavItem),
            self::TYPE_MODULE => $this->copyModuleItem($targetNavItem),
            self::TYPE_REDIRECT => $this->copyRedirectItem($targetNavItem),
            default => throw new Exception("Unable to find nav item type."),
        };
    }

    /**
     * Display all pages where the given module name is integrated.
     *
     * > Due to the module block which can integrate a module as well, we just return all the pages available.
     * > This method should be removed and not used.
     *
     * @param string $moduleName
     * @return \luya\cms\models\NavItem
     */
    public static function fromModule($moduleName)
    {
        return self::find()->all();
    }
}