luyadev/luya-module-admin

View on GitHub
src/ngrest/base/Plugin.php

Summary

Maintainability
C
7 hrs
Test Coverage
B
83%
<?php

namespace luya\admin\ngrest\base;

use luya\admin\base\TypesInterface;
use luya\admin\helpers\Angular;
use luya\admin\helpers\I18n;
use luya\Exception;
use luya\helpers\ArrayHelper;
use Yii;
use yii\base\Component;
use yii\helpers\Html;
use yii\helpers\Json;

/**
 * Base class for NgRest Plugins.
 *
 * + renderList, renderCreate, renderUpdate are execute once when the crud template is generated
 * + the on* methods are triggered for every model on the given events.
 *
 * Event trigger cycle for different use cases, the following use cases are available with its
 * event cycles.
 *
 * Async:
 * + onCollectServiceData: A collection bag where you can provide data and access them trough angular, its not available inside the same model as this process runs async
 *
 * Data foreach:
 * + onFind: The model is used in your controller frontend logic to display and assign data into the view (developer use case)
 * + onListFind: The model is populated for the Admin Table list view where you can see all your items and click the edit/delete icons.
 * + onExpandFind: Equals to onFind but only for the view api of the model, which means the data which is used for edit.
 * + onSave: Before Update / Create of the new data set.
 *
 * @property string|array $sortField Sort field definition (since 2.0.0)
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.0
 */
abstract class Plugin extends Component implements TypesInterface
{
    public const CREATE_CONTEXT_PREFIX = 'create.';

    public const UPDATE_CONTEXT_RPEFXI = 'update.';

    public const LIST_CONTEXT_PREFIX = 'item.';

    /**
     * @var string The name of the field corresponding to the ActiveRecord (also known as fieldname)
     */
    public $name;

    /**
     * @var string The alias name of the plugin choosen by the user (also known as label)
     */
    public $alias;

    /**
     * @var boolean Whether the plugin is in i18n context or not.
     */
    public $i18n = false;

    /**
     * @var boolean Whether the the value is only readable in EDIT scope or not. If enabled the data is displayed in the edit form as value instead
     * of the edit form. This has no effect to the create scope ony to the edit scope. Internally when enabled, the plugin will use {{renderList()}} in
     * update context instead of {{renderUpdate()}}.
     * @since 3.0.0
     */
    public $readonly = false;

    /**
     * @var boolean Whether this column should be hidden in the list. If the column is hidden in the list the data will be loaded from the api and can
     * be used by other fields, but its not visible in list view.
     * @since 2.0.0
     */
    public $hideInList = false;

    /**
     * @var mixed This value will be used when the i18n decodes the given value but is not set yet, default value.
     */
    public $i18nEmptyValue = '';

    /**
     * @var string Provide a condition in order to show or hide a given field. The condition relies on other fields from the forms. In order
     * to make sure the right context is used (create, update) put the fieldname into curly brackets `{field1}`.
     *
     * ```php
     * 'myText' => 'text',
     * 'otherText' => ['text', 'condition' => "{myText}"], // which is equals to when {myText} is not empt display the `otherText` field.
     * ```
     *
     * The above example would hide the `otherText` elment until `myText` is not empty. The condition is inside the `ng-show` element and the field
     * must be declared inside `{}` this will return the field name based on the current context like `data.create.myText` or `data.update.myText`.
     *
     * + display when not empty: `{field}`
     * + display when empty: `!{field}`
     * + display when has a given value: `{field}==1` (could be used when field is a select with values).
     *
     * @since 1.2.0
     */
    public $condition;

    /**
     * @var \luya\admin\ngrest\render\RenderCrudInterface The render context object when rendering
     * + {{Plugin::renderList()}}
     * + {{Plugin::renderCreate()}}
     * + {{Plugin::renderUpdate()}}
     * @since 1.1.1
     */
    public $renderContext;

    /**
     * @var callable A callable which will return before the ngrest list view for a given attribute.
     *
     * The function annotation contains the value of the attribute and second the model (event sender argument).
     *
     * ```php
     * 'beforeListFind' => function($value, $model) {
     *    return I18n::decode($value)['en']; // returns always the english language key value.
     * }
     * ```
     *
     * Return return value of the callable function will be set as attribute value to display.
     *
     * > Keep in mind that the return value of the function won't be processed by any further events.
     * > For example the i18n property might not have any effect anymore even when {{$i18n}} is enabled.
     *
     * @since 2.2.2
     */
    public $beforeListFind;

    /**
     * @var mixed A background color of a CRUD table cell where plugin data is shown.
     *
     * The value can be any string with CSS valid color such as `'red'` or `'#35FD87'` or `'rgba (124, 255, 23, 0.2)'`.
     * Or the value can be a simple AngularJS expression e.g. `'colorData'` or `'gender ? "purple" : "blue"'` assuming `colorData` or `gender` is declared in {{luya\admin\ngrest\base\NgRestModel::ngRestScopes()}} list scope.
     * In the second case the cell color will be automatically changed when the corresponding attribute changes.
     *
     * ```php
     * 'cellColor' => 'gender ? "purple" : "blue"'
     * ```
     *
     * @see https://docs.angularjs.org/guide/expression
     * @since 4.1.0
     */
    public $cellColor = false;

    /**
     * @var mixed An additional icon of a CRUD table column and CRUD edit form item.
     *
     * The value is the icon name based on https://material.io/icons or false if no icon is specified.
     *
     * ```php
     * 'icon' => 'account_circle'
     * ```
     *
     * @since 4.2.0
     */
    public $icon = false;

    /**
     * Renders the element for the CRUD LIST overview for a specific type.
     *
     * @param string $id The ID of the element in the current context
     * @param string $ngModel The name to access the data in angular context.
     * @return string|array Returns the html element as a string or an array which will be concated
     */
    abstract public function renderList($id, $ngModel);

    /**
     * Renders the element for the CRUD CREATE FORM for a specific type.
     *
     * @param string $id The ID of the element in the current context
     * @param string $ngModel The name to access the data in angular context.
     * @return string|array Returns the html element as a string or an array which will be concated
     */
    abstract public function renderCreate($id, $ngModel);

    /**
     * Renders the element for the CRUD UPDATE FORM for a specific type.
     *
     * @param string $id The ID of the element in the current context
     * @param string $ngModel The name to access the data in angular context.
     * @return string|array Returns the html element as a string or an array which will be concated
     */
    abstract public function renderUpdate($id, $ngModel);

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

        if ($this->name === null || $this->alias === null || $this->i18n === null) {
            throw new Exception("Plugin attributes name, alias and i18n must be configured.");
        }

        $this->addEvent(NgRestModel::EVENT_BEFORE_VALIDATE, 'onSave');
        $this->addEvent(NgRestModel::EVENT_AFTER_INSERT, 'onAssign');
        $this->addEvent(NgRestModel::EVENT_AFTER_UPDATE, 'onAssign');
        $this->addEvent(NgRestModel::EVENT_AFTER_REFRESH, 'onAssign');
        $this->addEvent(NgRestModel::EVENT_AFTER_FIND, 'onFind');
        $this->addEvent(NgRestModel::EVENT_AFTER_NGREST_FIND, 'onListFind');
        $this->addEvent(NgRestModel::EVENT_AFTER_NGREST_UPDATE_FIND, 'onExpandFind');
        $this->addEvent(NgRestModel::EVENT_SERVICE_NGREST, 'onCollectServiceData');
    }

    private $_sortField;

    /**
     * Setter method for sortField
     *
     * @param string|array $field A sort field definition, this can be either a string `firstname` or an array with a definition or multiple definitions
     *
     * ```php
     * 'sortField' => [
     *     'asc' => ['fist_name' => SORT_ASC, 'last_name' => SORT_ASC],
     *     'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
     * ]
     * ```
     *
     * Or you have an ngrest attribute which does not exists in the database, so you can define the original sorting attribute:
     *
     * ```php
     * 'sortField' => 'field_name_inside_the_table'
     * ```
     *
     * which is equals to
     *
     * ```php
     * 'sortField' => [
     *    'asc' => ['field_name_inside_the_table' => SORT_ASC],
     *    'desc' => ['field_name_inside_the_table' => SORT_DESC],
     * ]
     * ```
     *
     * A very common scenario when define sortField is when display a value from a relation, therefore you need to prepare the data provider
     * inside the API in order to **join** the relation (joinWith(['relationName])) aftewards you can use the table name
     *
     * ```php
     * 'sortField' => [
     *      'asc' => ['city_table.name' => SORT_ASC],
     *      'desc' => ['city_table.name' => SORT_DESC],
     * ]
     * ```
     *
     * There are situations you might turn of the sorting for the given attribute therefore just sortfield to false:
     *
     * ```php
     * 'sortField' => false,
     * ```
     *
     * In order to enable order by counting of relation tables update the APIs {{luya\admin\ngrest\base\Api::prepareListQuery()}} with the given sub
     * select condition where the alias name is equal to the field to sort:
     *
     * ```php
     * public function prepareListQuery()
     * {
     *     return parent::prepareListQuery()->select(['*', '(SELECT count(*) FROM admin_tag_relation WHERE tag_id = id) as relationsCount']);
     * }
     * ```
     *
     * Where in the above example `relationsCount` would be the sortField name.
     *
     * @see https://www.yiiframework.com/doc/api/2.0/yii-data-sort#$attributes-detail
     * @since 2.0.0
     */
    public function setSortField($field)
    {
        $this->_sortField = $field;
    }

    /**
     * Getter method for a sortField definition.
     *
     * If no sortField definition has been set, the plugin attribute name is used.
     *
     * @return array
     * @since 2.0.0
     */
    public function getSortField()
    {
        if ($this->_sortField === false) {
            return [];
        }

        if ($this->_sortField) {
            //
            if (is_array($this->_sortField)) {
                return [$this->name => $this->_sortField];
            }

            return [$this->name => [
                'asc' => [$this->_sortField => SORT_ASC],
                'desc' => [$this->_sortField => SORT_DESC]
            ]];
        }

        return [$this->name];
    }

    /**
     * Return the defined constant for a angular service instance in the current object.
     *
     * @param string $name The name of the service defined as array key in `serviceData()`.
     * @return string
     */
    public function getServiceName($name)
    {
        return 'service.'.$this->name.'.'.$name;
    }

    /**
     * Define the service data which will be called when creating the ngrest crud view. You may override
     * this method in your plugin.
     *
     * Example Response
     *
     * ```php
     * return [
     *     'titles' => ['mr, 'mrs'];
     * ];
     * ```
     *
     * The above service data can be used when creating the tags with `$this->getServiceName('titles')`.
     *
     * @param \yii\base\Event $event The event sender which triggers the event.
     * @return boolean|array
     */
    public function serviceData($event)
    {
        return false;
    }

    /**
     * Decodes the given JSON string into a PHP data structure and verifys if its empty, cause this can thrown an json decode exception.
     *
     * @param string $value The string to decode from json to php.
     * @return array The PHP array.
     */
    public function jsonDecode($value)
    {
        return empty($value) ? [] : Json::decode($value);
    }

    // I18N HELPERS

    /**
     * Encode from PHP to Json.
     *
     * See {{luya\admin\helpers\I18n::encode}}
     *
     * @param string|array $value
     * @return string Returns a string
     * @deprecated Deprecated since version 3.1, will trigger an deprecated warning in 4.0, will be removed in version 5.0
     */
    public function i18nFieldEncode($value)
    {
        trigger_error('deprecated, use I18n::encode instead. Will be removed in version 5.0', E_USER_DEPRECATED);
        return I18n::encode($value);
    }

    /**
     * Decode from Json to PHP.
     *
     * See {{luya\admin\helpers\I18n::decode}}
     *
     * @param string|array $value The value to decode (or if alreay is an array already)
     * @param string $onEmptyValue Defines the value if the language could not be found and a value will be returns, this value will be used.
     * @return array returns an array with decoded field value
     * @deprecated Deprecated since version 3.1, will trigger an deprecated warning in 4.0, will be removed in version 5.0
     */
    public function i18nFieldDecode($value, $onEmptyValue = '')
    {
        trigger_error('deprecated, use I18n::decode instead. Will be removed in version 5.0', E_USER_DEPRECATED);
        return I18n::decode($value, $onEmptyValue);
    }

    /**
     * Encode the current value from a language array.
     *
     * See {{luya\admin\helpers\I18n::findActive}}
     *
     * @param array $fieldValues
     * @return string
     * @deprecated Deprecated since version 3.1, will trigger an deprecated warning in 4.0, will be removed in version 5.0
     */
    public function i18nDecodedGetActive(array $fieldValues)
    {
        trigger_error('deprecated, use I18n::findActive instead. Will be removed in version 5.0', E_USER_DEPRECATED);
        return I18n::findActive($fieldValues);
    }

    // HTML TAG HELPERS

    /**
     * Wrapper for Yii Html::tag method
     *
     * @param string $name The name of the tag
     * @param string $content The value inside the tag.
     * @param array $options Options to passed to the tag generator.
     * @return string The generated html string tag.
     */
    public function createTag($name, $content, array $options = [])
    {
        return Html::tag($name, $content, $options);
    }

    /**
     * Preprends the context name for a certain ng model.
     *
     * As the angular attributes have different names in different contexts, this will append the correct context.
     *
     * ```php
     * $this->appendFieldNgModelContext('myfieldname', self::LIST_CONTEXT_PREFIX);
     * ```
     *
     * @param string $field
     * @param string $context
     * @return string
     */
    protected function appendFieldNgModelContext($field, $context)
    {
        return $context . ltrim($field, '.');
    }

    /**
      * Get the ng-show condition from a given ngModel context.
      *
      * Evaluates the ng-show condition from a given ngModel context. A condition like
      * `{field} == true` would return `data.create.field == true`.
      *
      * @param string $ngModel The ngModel to get the context informations from.
      * @return string Returns the condition with replaced field context like `data.create.fieldname == 0`
      * @since 1.2.0
      */
    public function getNgShowCondition($ngModel)
    {
        return Angular::variablizeContext($ngModel, $this->condition, false);
    }

    /**
     * Helper method to create a form tag based on current object.
     *
     * @param string $name Name of the form tag.
     * @param string $id The id tag of the tag.
     * @param string $ngModel The ngrest model name of the tag.
     * @param array $options Options to passes to the tag creator.
     * @return string The generated tag content.
     */
    public function createFormTag($name, $id, $ngModel, array $options = [])
    {
        $defaultOptions = [
            'fieldid' => $id,
            'model' => $ngModel,
            'label' => $this->alias,
            'fieldname' => $this->name,
            'i18n' => $this->i18n ? 1 : '',
        ];

        // if a condition is available, evalute from given context
        if ($this->condition) {
            $defaultOptions['ng-show'] = $this->getNgShowCondition($ngModel);
        }

        return $this->createTag($name, null, array_merge($options, $defaultOptions));
    }

    /**
     * Helper method to create a span tag with the ng-model in angular context for the crud overview
     * @param string $ngModel
     * @param array $options An array with options to pass to the list tag
     * @return string
     */
    public function createListTag($ngModel, array $options = [])
    {
        return $this->createTag('span', null, ArrayHelper::merge(['ng-bind' => $ngModel], $options));
    }

    /**
     * Create a tag for relation window toggler with directive crudLoader based on a ngrest model class.
     *
     * @param NgRestModel $ngrestModelClass
     * @param string|null $ngRestModelSelectMode
     * @param array $options
     * @param boolean $propagatePoolContext
     * @return string
     */
    public function createCrudLoaderTag($ngrestModelClass, $ngRestModelSelectMode = null, array $options = [], $propagatePoolContext = false)
    {
        if (!method_exists($ngrestModelClass, 'ngRestApiEndpoint')) {
            return null;
        }

        $menu = Yii::$app->adminmenu->getApiDetail($ngrestModelClass::ngRestApiEndpoint(), $propagatePoolContext ? Yii::$app->request->get('pool') : false);

        if ($menu) {
            if ($ngRestModelSelectMode) {
                $options['model-setter'] = $ngRestModelSelectMode;
                $options['model-selection'] = 1;
            } else {
                $options['model-selection'] = 0;
            }

            return $this->createTag('crud-loader', null, array_merge(['api' => $menu['route'], 'alias' => $menu['alias']], $options));
        }

        return null;
    }

    /**
     * Create the Scheulder tag for a given field.
     *
     * The scheduler tag allows you to change the given field value based on input values for a given field if a model is ailable.
     *
     * @param string $ngModel The string to
     * @param array $values An array with values to display
     * @param string $dataRow The data row context (item)
     * @param array $options `only-icon` => 1
     * @return string
     * @since 2.0.0
     */
    public function createSchedulerListTag($ngModel, array $values, $dataRow, array $options = [])
    {
        return Angular::schedule($ngModel, $this->alias, 'getRowPrimaryValue('.$dataRow.')', $values, $this->renderContext->getModel()::class, $this->name, $options)->render();
    }

    // EVENTS

    private $_events = [];

    /**
     * Add an event to the list of events
     *
     * @param string $trigger ActiveRecord event name
     * @param string $handler Method-Name inside this object
     */
    public function addEvent($trigger, $handler)
    {
        $this->_events[$trigger] = $handler;
    }

    /**
     * Remove an event from the events stack by its trigger name.
     *
     * In order to remove an event trigger from stack you have to do this right
     * after the initializer.
     *
     * ```php
     * public function init()
     * {
     *     parent::init();
     *     $this->removeEvent(NgRestModel::EVENT_AFTER_FIND);
     * }
     * ```
     *
     * @param string $trigger The event trigger name from the EVENT constants.
     */
    public function removeEvent($trigger)
    {
        if (isset($this->_events[$trigger])) {
            unset($this->_events[$trigger]);
        }
    }

    /**
     * An override without calling the parent::events will stop all other events used by default.
     *
     * @return array
     */
    public function events()
    {
        return $this->_events;
    }

    // ON SAVE

    /**
     * This event will be triggered before `onSave` event.
     *
     * @param \yii\db\AfterSaveEvent $event AfterSaveEvent represents the information available in yii\db\ActiveRecord::EVENT_AFTER_INSERT and yii\db\ActiveRecord::EVENT_AFTER_UPDATE.
     * @return boolean
     */
    public function onBeforeSave($event)
    {
        return true;
    }

    /**
    * This event will be triggered `onSave` event. If the model property is not writeable the event will not trigger.
    *
    * If the beforeSave method returns true and i18n is enabled, the value will be json encoded.
    *
    * @param \yii\base\ModelEvent $event ModelEvent represents the information available in yii\db\ActiveRecord::EVENT_BEFORE_VALIDATE.
    * @return void
    */
    public function onSave($event)
    {
        if ($this->isAttributeWriteable($event) && $this->onBeforeSave($event)) {
            if ($this->i18n) {
                $event->sender->setAttribute($this->name, I18n::encode($event->sender->getAttribute($this->name)));
            }
        }
    }

    // ON ASSIGN

    /**
     * After attribute value assignment.
     *
     * This event will trigger on:
     *
     * + AfterInsert
     * + AfterUpdate
     * + AfterRefresh
     *
     * The main purpose is to ensure html encoding when the model is not populated with after find from the database.
     *
     * @param \yii\base\ModelEvent $event When the database entry after insert, after update, after refresh.
     * @since 1.1.1
     */
    public function onAssign($event)
    {
    }

    // ON LIST FIND

    /**
     * This event will be triger before `onListFind`.
     *
     * @param \luya\admin\ngrest\base\NgRestModel::EVENT_AFTER_NGREST_FIND $event The NgRestModel after ngrest find event.
     * @return boolean
     */
    public function onBeforeListFind($event)
    {
        if ($this->beforeListFind) {
            $this->writeAttribute($event, call_user_func_array($this->beforeListFind, [$this->getAttributeValue($event), $event->sender]));
            return false;
        }

        return true;
    }

    /**
     * This event is only trigger when returning the ngrest crud list data.
     *
     * If the attribute is not inside the model (property not writeable), the event will not be triggered. Ensure its a
     * public property or contains getter/setter methods.
     *
     * @param \luya\admin\ngrest\base\NgRestModel::EVENT_AFTER_NGREST_FIND $event The NgRestModel after ngrest find event.
     */
    public function onListFind($event)
    {
        if ($this->isAttributeWriteable($event) && $this->onBeforeListFind($event)) {
            if ($this->i18n) {
                $event->sender->setAttribute(
                    $this->name,
                    I18n::decodeFindActive($event->sender->getAttribute($this->name), $this->i18nEmptyValue, Yii::$app->adminLanguage->defaultLanguageShortCode)
                );
            }
            $this->onAfterListFind($event);
        }
    }

    /**
     * This event will be triggered after `onListFind`.
     *
     * @param \luya\admin\ngrest\base\NgRestModel::EVENT_AFTER_NGREST_FIND $event The NgRestModel after ngrest find event.
     * @return boolean
     */
    public function onAfterListFind($event)
    {
        return true;
    }

    // ON FIND

    /**
     * This event will be trigger before `onFind`.
     *
     * @param \yii\base\Event $event An event that is triggered after the record is created and populated with query result.
     * @return boolean
     */
    public function onBeforeFind($event)
    {
        return true;
    }

    /**
     * ActiveRecord afterFind event. If the property of this plugin inside the model, the event will not be triggered.
     *
     * @param \yii\base\Event $event An event that is triggered after the record is created and populated with query result.
     */
    public function onFind($event)
    {
        if ($this->isAttributeWriteable($event) && $this->onBeforeFind($event)) {
            if ($this->i18n) {
                $oldValue = $event->sender->getAttribute($this->name);
                $event->sender->setI18nOldValue($this->name, $oldValue);
                // get the new array value from an i18n json attribute
                $value = I18n::decodeFindActive($oldValue, $this->i18nEmptyValue);
                // set the new attribute value
                $event->sender->setAttribute($this->name, $value);
                // override the old attribute value in order to ensure this attribute is not marked as dirty
                // see: https://github.com/luyadev/luya-module-admin/pull/567
                $event->sender->setOldAttribute($this->name, $value);
            }

            $this->onAfterFind($event);
        }
    }

    /**
     * This event will be trigger after `onFind`.
     *
     * @param \yii\base\Event $event An event that is triggered after the record is created and populated with query result.
     * @return boolean
     */
    public function onAfterFind($event)
    {
        return true;
    }

    // ON EXPAND FIND

    /**
     * This event will be triggered before `onExpandFind`.
     *
     * @param \luya\admin\ngrest\base\NgRestModel::EVENT_AFTER_NGREST_UPDATE_FIND $event NgRestModel event EVENT_AFTER_NGREST_UPDATE_FIND.
     * @return boolean
     */
    public function onBeforeExpandFind($event)
    {
        return true;
    }

    /**
     * NgRest Model crud list/overview event after find. If the property of this plugin inside the model, the event will not be triggered.
     * @param \luya\admin\ngrest\base\NgRestModel::EVENT_AFTER_NGREST_UPDATE_FIND $event NgRestModel event EVENT_AFTER_NGREST_UPDATE_FIND.
     */
    public function onExpandFind($event)
    {
        if ($this->isAttributeWriteable($event) && $this->onBeforeExpandFind($event)) {
            if ($this->i18n) {
                $event->sender->setAttribute(
                    $this->name,
                    I18n::decode($event->sender->getAttribute($this->name), $this->i18nEmptyValue)
                );
            }

            $this->onAfterExpandFind($event);
        }
    }

    /**
     * This event will be triggered after `onExpandFind`.
     *
     * @param \luya\admin\ngrest\base\NgRestModel::EVENT_AFTER_NGREST_UPDATE_FIND $event NgRestModel event EVENT_AFTER_NGREST_UPDATE_FIND.
     * @return boolean
     */
    public function onAfterExpandFind($event)
    {
        return true;
    }

    // ON COLLECT SERVICE DATA

    /**
     * This event will be triggered before `onCollectServiceData`.
     *
     * @param \luya\admin\ngrest\base\NgRestModel::EVENT_SERVICE_NGREST $event NgRestModel event EVENT_SERVICE_NGREST.
     * @return boolean
     */
    public function onBeforeCollectServiceData($event)
    {
        return true;
    }

    /**
     * The ngrest services collector.
     *
     * > The service event is async to the other events, which means the service event collects data before the other events are called.
     *
     * @param \luya\admin\ngrest\base\NgRestModel::EVENT_SERVICE_NGREST $event NgRestModel event EVENT_SERVICE_NGREST.
     */
    public function onCollectServiceData($event)
    {
        if ($this->onBeforeCollectServiceData($event)) {
            $data = $this->serviceData($event);
            if (!empty($data)) {
                $event->sender->addNgRestServiceData($this->name, $data);
            }
        }
    }

    /**
     * Check whether the current plugin attribute is writeable in the Model class or not. If not writeable some events will be stopped from
     * further processing. This is mainly used when adding extraFields to the grid list view.
     *
     * @param \yii\base\Event $event The current base event object.
     * @return boolean Whether the current plugin attribute is writeable or not.
     */
    protected function isAttributeWriteable($event)
    {
        return ($event->sender->hasAttribute($this->name) || $event->sender->canSetProperty($this->name));
    }

    /**
     * Write a value to a plugin attribute or property.
     *
     * As setAttribute() does only write to attributes therefore this method allwos you to write to
     * a property or attribute value. As {{isAttributeWriteAble()}} returns true whether its a property
     * or attribute.
     *
     * @param \yii\base\Event $event The event to retrieve the values from (via $sender property).
     * @param mixed $value The value to writte on the attribute or property.
     * @since 1.2.1
     */
    protected function writeAttribute($event, $value)
    {
        $property = $this->name;
        $event->sender->{$property} = $value;
    }

    /**
     * Get the value from the plugin attribute or property.
     *
     * @param \yii\base\Event $event The event to retrieve the values from (via $sender property).
     * @return mixed
     * @since 1.2.1
     */
    protected function getAttributeValue($event)
    {
        $property = $this->name;

        return $event->sender->{$property};
    }
}