SlugBehavior.php 11.3 KB
<?php
    
    namespace artbox\core\behaviors;
    
    use artbox\core\helpers\SlugHelper;
    use artbox\core\models\Alias;
    use yii;
    use yii\base\Behavior;
    use yii\base\ErrorException;
    use yii\db\ActiveRecord;
    use yii\helpers\Json;
    
    /**
     * Class SlugBehavior
     * Behavior to generate and maintain connection with aliases.
     *
     * @property ActiveRecord $owner
     */
    class SlugBehavior extends Behavior
    {
        
        /**
         * Attribute from which slug should be generated
         *
         * @var string
         */
        public $inAttribute = 'title';
        
        /**
         * Attribute which holds link to Alias
         *
         * @var int
         */
        public $outAttribute = 'alias_id';
    
        /**
         * Action which should be called after parsing Url
         *
         * @var string
         */
        public $action;
    
        /**
         * Params which should be passed to controller action
         *
         * @var array
         */
        public $params;
    
        /**
         * Whether to use transit or just replace non-alphabetic symbols.
         *
         * @var bool
         */
        public $translit = true;
    
        /**
         * Fields available in for SEO.
         *
         * @var array
         */
        public $fields = [];
    
        /**
         * Get Alias $value for $owner.
         * If needed db request would be used only once.
         *
         * @var string $aliasValue
         */
        protected $aliasValue = null;
    
        /**
         * Current Alias model for $owner.
         *
         * @var Alias|null $currentAlias
         */
        private $currentAlias;
    
        /**
         * Whether db query to get Alias already executed, means that request won't be made until force flag is set.
         *
         * @var bool $executed
         */
        private $executed = false;
        
        /**
         * @inheritdoc
         */
        public function events()
        {
            return [
                ActiveRecord::EVENT_AFTER_INSERT  => 'getSlug',
                ActiveRecord::EVENT_AFTER_UPDATE  => 'getSlug',
                ActiveRecord::EVENT_BEFORE_DELETE => 'deleteSlug',
            ];
        }
        
        /**
         * @inheritdoc
         */
        public function attach($owner)
        {
            parent::attach($owner);
            if (!$owner instanceof ActiveRecord) {
                throw new yii\base\InvalidConfigException(
                    \Yii::t('core', 'SlugBeahvior can only be attached to the instance of ActiveRecord')
                );
            }
        }
        
        /**
         * Generate slug
         *
         * @param yii\base\Event $event
         *
         * @return void
         */
        public function getSlug($event)
        {
            /**
             * @var string $attribute_value
             */
            $attribute_value = $this->owner->getAttribute($this->inAttribute);
            if (!empty($attribute_value)) {
                /**
                 * @var string $alias_value
                 */
                $alias_value = $this->aliasValue;
                $this->currentAlias = $this->findCurrentAlias();
                if (empty($alias_value)) {
                    $result = $this->generateSlug($attribute_value);
                } else {
                    $result = $this->generateSlug($alias_value);
                }
                $this->saveSlug($result);
            }
        }
    
        /**
         * Delete slug
         *
         * @param yii\base\Event $event
         *
         * @return void
         */
        public function deleteSlug($event)
        {
            if (!empty($current = $this->findCurrentAlias())) {
                $current->delete();
            }
        }
    
        /**
         * Get current Alias. Db query will be made first time or if $force flag is set.
         *
         * @param bool $force
         *
         * @return \artbox\core\models\Alias|null
         */
        public function findCurrentAlias($force = false)
        {
            if ($force || !$this->executed) {
                /**
                 * @var int $alias_id
                 */
                $alias_id = $this->owner->getAttribute($this->outAttribute);
                $currentAlias = null;
                if (!empty($alias_id)) {
                    /**
                     * @var Alias|null $currentAlias
                     */
                    $currentAlias = Alias::find()
                                         ->where(
                                             [
                                                 'id' => $alias_id,
                                             ]
                                         )
                                         ->one();
                }
                $this->executed = true;
                return $currentAlias;
            } else {
                return $this->currentAlias;
            }
        }
    
        /**
         * Generate unique slug.
         *
         * @param string $slug
         *
         * @return string
         */
        private function generateSlug($slug)
        {
            $slug = $this->slugify($slug);
            $languageId = null;
            if ($this->owner->hasAttribute('language_id')) {
                $languageId = $this->owner->getAttribute('language_id');
            }
            return SlugHelper::unify($slug, $this->currentAlias, $languageId);
        }
    
        /**
         * Generate slug from string according to $translit flag.
         *
         * @param string $slug
         *
         * @return string
         */
        private function slugify($slug)
        {
            if ($this->translit) {
                return SlugHelper::slugify($slug);
            } else {
                return $this->slug($slug, '-', true);
            }
        }
    
        /**
         * Replace non-alphabetic symbols with $replcement symbol.
         *
         * @param string $string
         * @param string $replacement
         * @param bool   $lowercase
         *
         * @return string
         */
        private function slug($string, $replacement = '-', $lowercase = true)
        {
            $string = preg_replace('/[^\p{L}\p{Nd}]+/u', $replacement, $string);
            $string = trim($string, $replacement);
            return $lowercase ? strtolower($string) : $string;
        }
    
        /**
         * Check if $slug differs from current Alias value
         *
         * @param $slug
         *
         * @return bool
         */
        private function hasChanged($slug)
        {
            if (!empty($this->currentAlias) && $this->currentAlias->value === $slug) {
                return false;
            } else {
                return true;
            }
        }
    
        /**
         * If $slug differs from Db Alias value, updates current if exists or create new Alias row.
         *
         * @param $slug
         *
         * @throws \yii\base\ErrorException
         */
        protected function saveSlug($slug)
        {
            if (!$this->hasChanged($slug)) {
                return;
            } else {
                $route = $this->buildRoute();
                $owner = $this->owner;
                if (empty($alias = $this->findCurrentAlias())) {
                    /**
                     * @var Alias $alias
                     */
                    $alias = \Yii::createObject(
                        Alias::className(),
                        [
                            [
                                'value'       => $slug,
                                'route'       => $route,
                                'entity'      => $owner::className(),
                                'title'       => $owner->getAttribute($this->inAttribute),
                                'h1'          => $owner->getAttribute($this->inAttribute),
                                'fields'      => Json::encode($this->fields),
                                'language_id' => $owner->getAttribute('language_id'),
                            ],
                        ]
                    );
        
                } else {
                    $alias->value = $slug;
                    $alias->route = $route;
                    $alias->entity = $owner::className();
                    $alias->language_id = $owner->getAttribute('language_id');
                }
                if ($alias->save()) {
                    $owner->{$this->outAttribute} = $alias->id;
                    $this->detach();
                    $owner->save(false, [ $this->outAttribute ]);
                } else {
                    $alias = Alias::find()
                                  ->where(
                                      [
                                          'route'       => $alias->route,
                                          'language_id' => $alias->language_id,
                                      ]
                                  )
                                  ->one();
                    if ($alias) {
                        $owner->{$this->outAttribute} = $alias->id;
                        $this->detach();
                        $owner->save(false, [ $this->outAttribute ]);
                    } else {
                        throw new ErrorException(
                            \Yii::t(
                                'core',
                                'Alias cannot be saved: ' . Json::encode($alias) . '. Errors: ' . Json::encode(
                                    $alias->getErrors()
                                )
                            )
                        );
                    }
                }
            }
        }
        
        /**
         * Get $aliasValue. Try to get from cached value and tries to get from Db if empty.
         *
         * @return string
         */
        public function getAliasValue()
        {
            if (empty($this->aliasValue) && !$this->executed) {
                $currentAlias = $this->findCurrentAlias();
                if (!empty($currentAlias)) {
                    $this->aliasValue = $currentAlias->value;
                    return $currentAlias->value;
                }
            }
            return $this->aliasValue;
        }
    
        /**
         * Set $aliasValue
         *
         * @param $value
         */
        public function setAliasValue($value)
        {
            $this->aliasValue = $value;
        }
    
        /**
         * Build Json route from $action and $params fields.
         *
         * @return null|string
         */
        protected function buildRoute()
        {
            $owner = $this->owner;
            $route = [];
            $action = $this->action;
            if (empty($action)) {
                return null;
            } else {
                $route[ 0 ] = $action;
            }
            $params = $this->params;
            if (!empty($params) && is_array($params)) {
                foreach ($params as $param => $attribute) {
                    $attributeValue = $owner->getAttribute($attribute);
                    if (!empty($attributeValue)) {
                        $route[ $param ] = $attributeValue;
                    }
                }
            }
            return Json::encode($route);
        }
    }