luyadev/luya-module-admin

View on GitHub
src/ngrest/plugins/CheckboxRelation.php

Summary

Maintainability
A
3 hrs
Test Coverage
C
75%
<?php

namespace luya\admin\ngrest\plugins;

use luya\admin\helpers\I18n;
use luya\admin\ngrest\base\NgRestModel;
use luya\admin\ngrest\base\Plugin;
use luya\admin\ngrest\traits\CrudLoaderPluginTrait;
use luya\helpers\ArrayHelper;
use luya\rest\ActiveController;
use Yii;

/**
 * Checkbox Selector via relation table.
 *
 * The checkbox relation plugin provides an easy way to provde a checkbox selection from a via relation table.
 *
 * In order to implement the checkboxRealtion plugin you have to prepare your {{\luya\admin\ngrest\base\NgRestModel}} as following:
 *
 * ```php
 * public $groups = [];
 *
 * public function extraFields()
 * {
 *     return ['groups'];
 * }
 * ```
 *
 * Configure the extra field with {{\luya\admin\ngrest\base\NgRestModel::ngRestExtraAttributeTypes}}:
 *
 * ```php
 * public function ngRestExtraAttributeTypes()
 * {
 *     'groups' => [
 *         'checkboxRelation',
 *         'model' => User::className(),
 *         'refJoinTable' => 'admin_user_group',
 *         'refModelPkId' => 'group_id',
 *         'refJoinPkId' => 'user_id',
 *         'labelField' => ['firstname', 'lastname', 'email'],
 *         'labelTemplate' =>  '%s %s (%s)'
 *     ],
 * }
 * ```
 *
 * You can also access getter fields from the $model class in order to display such informations in the checkbox selection. Assuming you have a `getMyName` method in the
 * $model object you can use it in the `labelField` as `myName`.
 *
 * In order to use a function for the labelField:
 *
 * ```php
 * 'labelField' => function($model) {
 *     return $model['firstname'] . ' ' . $model['lastname'];
 * }
 * ```
 *
 * If you want to output an i18n field:
 * ```php
 * 'labelField' => function($model) {
 *     return I18n::decodeFindActive($model["i18nfield"]);
 * }
 * ```
 *
 * In case you want to get an active query item for the checkboxRelation data you can add a relation getter method which will be used to collect to data.
 *
 * ```php
 * public function getGroups()
 * {
 *     return $this->hasMany(User::class, ['id' => 'user_id'])->viaTable('admin_user_grou', ['group_id' => 'id']);
 * }
 * ```
 *
 * As now there is a relation for `groups` this relation query will be used in order to return the data.
 *
 * @property \luya\admin\ngrest\base\NgRestModel $model The model object
 * @property string $modelPrimaryKey The primary key string.
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.0
 */
class CheckboxRelation extends Plugin
{
    use CrudLoaderPluginTrait;

    /**
     * @var string The reference table table name e.g. `admin_user_groupadmin_user_group`.
     */
    public $refJoinTable;

    /**
     * @var string The reference table model field name e.g `group_id`.
     */
    public $refModelPkId;

    /**
     * @var string The reference table poin field name e.g. `user_id`.
     */
    public $refJoinPkId;

    /**
     * @var array A list of fields which should be used for the display template. Can also be a callable function to build the field with the template
     *
     * ```php
     * 'labelField' => function($array) {
     *     return $array['firstname'] . ' ' . $array['lastname'];
     * }
     * ```
     */
    public $labelField;

    /**
     * @var string The template which is sued for the label fields like the sprinf command e.g. `%s %s (%s)`.
     */
    public $labelTemplate;

    /**
     * @var boolean Whether the checkbox plugin should only trigger for the restcreate and restupdate events or for all SAVE/UPDATE events.
     */
    public $onlyRestScenarios = false;

    /**
     * @var boolean Whether the admin request should be retrived as admin or not, by default its an admin query as this is more performat but
     * you only have an array within the labelField closure.
     */
    public $asArray = true;

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        $this->addEvent(NgRestModel::EVENT_AFTER_INSERT, [$this, 'afterSaveEvent']);
        $this->addEvent(NgRestModel::EVENT_AFTER_UPDATE, [$this, 'afterSaveEvent']);
    }

    private $_modelPrimaryKey;

    public function getModelPrimaryKey()
    {
        if ($this->_modelPrimaryKey === null) {
            $pkname = $this->model->primaryKey();
            $this->_modelPrimaryKey = reset($pkname);
        }

        return $this->_modelPrimaryKey;
    }

    private $_model;

    /**
     * Setter method for the model.
     *
     * @param string $className
     */
    public function setModel($className)
    {
        $this->_model = $className;
    }

    /**
     * Getter method for model.
     *
     * @return \yii\base\Model
     */
    public function getModel()
    {
        if (!is_object($this->_model)) {
            $this->_model = Yii::createObject(['class' => $this->_model]);
        }

        return $this->_model;
    }

    /**
     * @inheritdoc
     */
    public function renderList($id, $ngModel)
    {
        return $this->createListTag($ngModel);
    }

    /**
     * @inheritdoc
     */
    public function renderCreate($id, $ngModel)
    {
        return [
            $this->createCrudLoaderTag($this->model::class, null, [], $this->crudLoaderPoolContext),
            $this->createFormTag('zaa-checkbox-array', $id, $ngModel, ['options' => $this->getServiceName('relationdata')]),
        ];
    }

    /**
     * @inheritdoc
     */
    public function renderUpdate($id, $ngModel)
    {
        return $this->renderCreate($id, $ngModel);
    }

    /**
     * Get the options data to display.
     *
     * @return array
     */
    private function getOptionsData($event)
    {
        $items = [];
        $targetModel = new $this->model();
        foreach ($this->model->find()->asArray($this->asArray)->all() as $item) {
            if (is_callable($this->labelField, false)) {
                $label = call_user_func($this->labelField, $item);
            } else {
                if ($this->labelField === null) {
                    $this->labelField = array_keys($item);
                }
                $array = ArrayHelper::filter($item, $this->labelField);

                foreach ($array as $key => $value) {
                    if ($targetModel->isI18n($key)) {
                        $array[$key] = I18n::decodeFindActive($value);
                    }
                }

                $label = $this->labelTemplate ? vsprintf($this->labelTemplate, $array) : implode(', ', $array);
            }

            $items[] = ['value' => (int) $item[$this->modelPrimaryKey], 'label' => $label];
        }

        return ['items' => $items];
    }

    /**
     * @inheritdoc
     */
    public function serviceData($event)
    {
        return ['relationdata' => $this->getOptionsData($event)];
    }

    /**
     * @inheritdoc
     */
    public function onBeforeExpandFind($event)
    {
        $data = [];
        foreach ($this->model->find()
            ->leftJoin($this->refJoinTable, $this->model->tableName().'.id='.$this->refJoinTable.'.'.$this->refJoinPkId)
            ->where([$this->refJoinTable.'.'.$this->refModelPkId => $event->sender->id])
            ->asArray(true)
            ->each() as $item) {
            $data[] = ['value' => $item[$this->getModelPrimaryKey()]];
        }
        $event->sender->{$this->name} = $data;

        return true;
    }

    /**
     * @inheritdoc
     */
    public function onBeforeFind($event)
    {
        if (!$this->onlyRestScenarios) {
            $event->sender->{$this->name} = $this->getRelationData($event);
        }

        return true;
    }

    private function getRelationData($event)
    {
        $relation = $event->sender->getRelation($this->name, false);
        if ($relation) {
            return $relation;
        }

        return $this->model->find()->leftJoin($this->refJoinTable, $this->model->tableName().'.id='.$this->refJoinTable.'.'.$this->refJoinPkId)->where([$this->refJoinTable.'.'.$this->refModelPkId => $event->sender->id])->all();
    }

    /**
     * Tiggers after Save/Update methods.
     *
     * ```php
     * $this->addEvent(NgRestModel::EVENT_AFTER_INSERT, [$this, 'afterSaveEvent']);
     * $this->addEvent(NgRestModel::EVENT_AFTER_UPDATE, [$this, 'afterSaveEvent']);
     * ```
     *
     * @param \yii\base\Event $event An ActiveRecord  event.
     */
    public function afterSaveEvent($event)
    {
        if ($this->onlyRestScenarios) {
            if ($event->sender->scenario == ActiveController::SCENARIO_RESTCREATE || $event->sender->scenario == ActiveController::SCENARIO_RESTUPDATE) {
                $this->setRelation($event->sender->{$this->name}, $this->refJoinTable, $this->refModelPkId, $this->refJoinPkId, $event->sender->id);
            }
        } else {
            $this->setRelation($event->sender->{$this->name}, $this->refJoinTable, $this->refModelPkId, $this->refJoinPkId, $event->sender->id);
        }
    }

    /**
     * Set the relation data based on the configuration.
     *
     * @param array $value The valued which is provided from the setter method
     * @param string $viaTableName Example viaTable name: news_article_tag
     * @param string $localTableId The name of the field inside the viaTable which represents the match against the local table, example: article_id
     * @param string $foreignTableId The name of the field inside the viaTable which represents the match against the foreign table, example: tag_id
     * @return boolean Whether updating the database was successful or not.
     */
    protected function setRelation(array $value, $viaTableName, $localTableId, $foreignTableId, $activeRecordId)
    {
        Yii::$app->db->createCommand()->delete($viaTableName, [$localTableId => $activeRecordId])->execute();
        $batch = [];
        foreach ($value as $k => $v) {
            // $this->id: the value of the current database model, example when relation ins on user model id would be user id
            // $v['id'] extra field values foreached from the join table, so id will represent the joined table pk.

            // issue #696 array logic
            if (is_array($v)) { // its an array and is based on the logic of the angular checkbox releation ['id' => 123] // new: 'value' => 123 since beta6
                if (isset($v['value'])) {
                    $batch[] = [$activeRecordId, $v['value']];
                }
            } else { // its not an array so it could have been assigned from the frontend
                $batch[] = [$activeRecordId, $v];
            }
        }
        if (!empty($batch)) {
            Yii::$app->db->createCommand()->batchInsert($viaTableName, [$localTableId, $foreignTableId], $batch)->execute();
        }
        return true;
    }
}