LinkBehavior.php 6.23 KB
<?php
    namespace artbox\core\behaviors;
    
    use yii\base\Behavior;
    use yii\base\Event;
    use yii\base\InvalidConfigException;
    use yii\base\Object;
    use yii\db\ActiveRecord;
    use yii\web\NotFoundHttpException;

    /**
     * Class LinkBehavior
     * Behavior to create linked object after inserting parent object.
     */
    class LinkBehavior extends Behavior
    {
        /**
         * Key in parent object to link. Default to primaryKey()[0]
         *
         * @var string $parentKey
         */
        public $parentKey;
    
        /**
         * Fully-namespaced class to be linked. Required.
         *
         * @var string $linkClass
         */
        public $linkClass;
    
        /**
         * Key in link object. Default to primaryKey()[0]
         *
         * @var string $linkKey
         */
        public $linkKey;
    
        /**
         * Named behaviors to be detached from link object before insertion.
         *
         * @var array
         */
        public $detachBehaviors = [];
    
        /**
         * @var ActiveRecord $linkClassInstance
         */
        protected $linkClassInstance;
    
        /**
         * @inheritdoc
         */
        public function events()
        {
            return [
                ActiveRecord::EVENT_AFTER_INSERT => 'linkObject',
            ];
        }
    
        /**
         * @inheritdoc
         */
        public function attach($owner)
        {
            parent::attach($owner);
            $this->checkValidity();
        }
    
        /**
         * Save link object.
         *
         * @param Event $event Handler for insertion success
         *
         * @return \yii\db\ActiveRecord
         * @throws \yii\base\InvalidConfigException
         */
        public function linkObject($event = null)
        {
            $owner = $this->owner;
            /**
             * @var ActiveRecord $linkClassInstance
             */
            $linkClassInstance = $this->linkClassInstance;
            $value = null;
            if ($owner->canGetProperty($this->parentKey, true, false)) {
                $key = $this->parentKey;
                $value = $owner->$key;
            } else {
                throw new InvalidConfigException(\Yii::t('core', 'Can\'t get $parentKey property'));
            }
            if (empty( $value )) {
                throw new InvalidConfigException(\Yii::t('core', "Can't get parent key {$key}"));
            }
            $linkClassInstance->setAttribute($this->linkKey, $value);
            foreach ($this->detachBehaviors as $name) {
                if (is_string($name)) {
                    $linkClassInstance->detachBehavior($name);
                }
            }
            $linkClassInstance->save();
            return $linkClassInstance;
        }
    
        /**
         * Check if $linkClassInstance ready to be inserted, otherwise throw exception
         *
         * @throws \yii\base\InvalidConfigException
         */
        protected function checkValidity()
        {
            $owner = $this->owner;
            if (empty( $this->linkClass )) {
                throw new InvalidConfigException(\Yii::t('core', 'Link class must be set'));
            } else {
                $this->linkClassInstance = \Yii::createObject($this->linkClass);
                if (!$this->linkClassInstance instanceof ActiveRecord) {
                    throw new InvalidConfigException(
                        \Yii::t('core', 'Link class must be instance of yii\db\ActiveRecord')
                    );
                }
            }
            /** @noinspection PhpParamsInspection */
            $this->ensureAttribute('parentKey', $owner);
            /** @noinspection PhpParamsInspection */
            $this->ensureAttribute('linkKey', $this->linkClassInstance);
            if (empty( $this->parentKey ) || empty( $this->linkKey )) {
                throw new InvalidConfigException(
                    \Yii::t(
                        'core',
                        '$parentKey and $linkKey must be set or use ActiveRecord to get its primaryKey()[0] attributes.'
                    )
                );
            }
            if (!$owner->canGetProperty($this->parentKey, true, false)) {
                throw new InvalidConfigException(\Yii::t('core', 'Can\'t get $parentKey property'));
            }
        }
    
        /**
         * Bind keys to default properties if keys empty.
         *
         * @param string $property Property to bind
         * @param Object $object   Property owner
         */
        protected function ensureAttribute(string $property, Object $object)
        {
            if (empty( $this->$property )) {
                if ($object instanceof ActiveRecord) {
                    $pks = $object->primaryKey();
                    if (!empty( $pks )) {
                        $this->$property = $pks[ 0 ];
                    }
                }
            }
        }
    
        /**
         * Generate link object an d save it directly.
         *
         * @return ActiveRecord
         */
        public function ensureExistance()
        {
            $link = $this->getLinkObject();
            if (!$link) {
                $link = $this->linkObject();
            }
            return $link;
        }
    
        /**
         * Get link object
         *
         * @param bool $throw Whether to throw exception if link object not found
         *
         * @return null|\yii\db\ActiveRecord
         * @throws \yii\web\NotFoundHttpException
         */
        public function getLinkObject(bool $throw = false)
        {
            /**
             * @var ActiveRecord $linkClassInstance
             */
            $linkClassInstance = $this->linkClassInstance;
            $owner = $this->owner;
            $parentKey = $this->parentKey;
            /**
             * @var ActiveRecord $result
             */
            $result = $linkClassInstance::find()
                                        ->where([ $this->linkKey => $this->owner->$parentKey ])
                                        ->one();
            if ($throw && !$result) {
                throw new NotFoundHttpException(\Yii::t('core', "Linked object for {$owner::className()} not found"));
            }
            return $result;
        }
    }