src/ngrest/base/Api.php
<?php
namespace luya\admin\ngrest\base;
use luya\admin\base\RestActiveController;
use luya\admin\components\Auth;
use luya\admin\models\Tag;
use luya\admin\models\UserAuthNotification;
use luya\admin\models\UserOnline;
use luya\admin\ngrest\Config;
use luya\admin\ngrest\NgRest;
use luya\admin\ngrest\render\RenderActiveWindow;
use luya\admin\ngrest\render\RenderActiveWindowCallback;
use luya\admin\traits\TaggableTrait;
use luya\helpers\ArrayHelper;
use luya\helpers\ExportHelper;
use luya\helpers\Json;
use luya\helpers\ObjectHelper;
use luya\helpers\StringHelper;
use luya\helpers\Url;
use Yii;
use yii\base\ErrorException;
use yii\base\InvalidCallException;
use yii\base\InvalidConfigException;
use yii\data\ActiveDataProvider;
use yii\db\ActiveQuery;
use yii\db\ActiveQueryInterface;
use yii\helpers\Html;
use yii\helpers\Inflector;
use yii\web\NotFoundHttpException;
/**
* The RestActiveController for all NgRest implementations.
*
* When pagination is enabled (by setting {{$pagination}} property or if {{$autoEnablePagination}} apply) the crud search will be performed trough an
* async request instead of angular filtering. Angular filtering is searching for the string in the response, while async full search does
* call the {{actionFullResponse()}} method trough the api, which will the call the {{luya\admin\ngrest\base\NgRestModel::ngRestFullQuerySearch()}} method.
*
* @property \luya\admin\ngrest\NgRestModel $model Get the model object based on the $modelClass property.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Api extends RestActiveController
{
/**
* @var string Defines the related model for the NgRest Controller. The full qualiefied model name
* is required.
*
* ```php
* public $modelClass = 'admin\models\User';
* ```
*/
public $modelClass;
/**
* @var array An array with default pagination configuration
* @since 1.2.2
*/
public $pagination = ['defaultPageSize' => 25];
/**
* @var string When a filter model is provided filter is enabled trough json request body, works only for index and list.
* @see https://luya.io/guide/ngrest/api.html#filtering
* @see https://www.yiiframework.com/doc/guide/2.0/en/output-data-providers#filtering-data-providers-using-data-filters
* @since 1.2.2
*/
public $filterSearchModelClass;
/**
* @var array|string Define a yii caching depency will enable the caching for this API.
*
* Example usage:
*
* ```php
* public $cacheDependency = [
* 'class' => 'yii\caching\DbDependency',
* 'sql' => 'SELECT MAX(update_ad) FROM news',
* ];
* ```
*
* This should be used very carefully as the ngrest crud list data is cached too. Therefore this should be used together
* with a Timestamp behavior!
*
* @see https://www.yiiframework.com/doc/guide/2.0/en/caching-data#cache-dependencies
* @since 1.2.3
*/
public $cacheDependency;
/**
* @var boolean If enabled, the truncate action is attached to the API. In order to run the truncate action the delete permission is required.
* @since 3.2.0
*/
public $truncateAction = false;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
if ($this->modelClass === null) {
throw new InvalidConfigException("The property `modelClass` must be defined by the Controller.");
}
$this->addActionPermission(Auth::CAN_VIEW, [
'index', 'view', 'services', 'search', 'relation-call', 'filter', 'export', 'list', 'toggle-notification',
]);
$this->addActionPermission(Auth::CAN_CREATE, [
'create',
]);
$this->addActionPermission(Auth::CAN_UPDATE, [
'active-window-render', 'active-window-callback', 'active-button', 'update',
]);
$this->addActionPermission(Auth::CAN_DELETE, [
'delete', 'truncate',
]);
}
/**
* Auto add those relations to queries.
*
* > The relation will only eager load when available in **expand** get params. `?expand=user` f.e.
*
* This can be either an array with relations which will be passed to `index, list and view` or an array with a subdefinition in order to define
* which relation should be us when.
*
* basic:
*
* ```php
* return ['user', 'images'];
* ```
*
* The above relations will be auto added trough {{yii\db\ActiveQuery::with()}}. In order to define view specific actions:
*
* ```php
* return [
* 'index' => ['user', 'images'],
* 'list' => ['user'],
* ];
* ```
*
* Possible action column names:
*
* + index
* + list
* + search
* + export
*
* @return array
* @since 1.2.2
*/
public function withRelations()
{
return [];
}
/**
* Get the relations for the corresponding action name.
*
* > Since version 1.2.3 it also checks if the $expand get param is provided for the given relations, otherwise
* > the relation will not be joined trough `with`. This reduces the database querie time.
*
* @param string $actionName The action name like `index`, `list`, `search`, `relation-call`.
* @return array An array with relation names.
* @since 1.2.2
*/
public function getWithRelation($actionName)
{
$expand = Yii::$app->request->get('expand', '');
$relationPrefixes = [];
foreach (StringHelper::explode($expand, ',', true, true) as $relation) {
// check for subrelation dot notation.
$relationPrefixes[] = current(explode(".", $relation));
}
// no expand param found, return empty array which means no relations to load
if (empty($relationPrefixes)) {
return [];
}
$withRelations = $this->withRelations();
foreach ($withRelations as $relationName) {
// it seem to be the advance strucutre for given actions.
if (is_array($relationName) && isset($withRelations[$actionName])) {
return $this->relationsFromExpand($withRelations[$actionName], $relationPrefixes);
}
}
// simple structure
return $this->relationsFromExpand($withRelations, $relationPrefixes);
}
/**
* Ensure if the expand prefix exists in the relation.
*
* @param array $relations The available relations
* @param array $expandPrefixes The available expand relation names
* @return array
* @since 1.2.3
*/
private function relationsFromExpand(array $relations, array $expandPrefixes)
{
$valid = [];
foreach ($expandPrefixes as $prefix) {
foreach ($relations as $relation) {
if (StringHelper::startsWith($relation, $prefix)) {
$valid[] = $relation;
}
}
}
return $valid;
}
/**
* Prepare Index Query.
*
* You can override the prepare index query to preload relation data like this:
*
* ```php
* public function prepareIndexQuery()
* {
* return parent::prepareIndexQuery()->with(['relation1', 'relation2']);
* }
* ```
*
* Make sure to call the parent implementation.
*
* > This will call the `find()` method of the model.
*
* @return \yii\db\ActiveQuery
* @since 1.2.1
*/
public function prepareIndexQuery()
{
/* @var $modelClass \yii\db\BaseActiveRecord */
$modelClass = $this->modelClass;
return $modelClass::find()->with($this->getWithRelation('index'));
}
/**
* Prepare the NgRest List Query.
*
* > This will call the `ngRestFind()` method of the model.
*
* Use in list, export
*
* @see {{prepareIndexQuery()}}
* @return \yii\db\ActiveQuery
* @since 1.2.2
*/
public function prepareListQuery()
{
/* @var $modelClass \yii\db\BaseActiveRecord */
$modelClass = $this->modelClass;
$find = $modelClass::ngRestFind();
$this->handleNotifications($modelClass, $this->authId);
// check if a pool id is requested:
$this->appendPoolWhereCondition($find);
// add tags condition
$tagIds = Yii::$app->request->get('tags');
if ($tagIds) {
$subQuery = clone $find;
$inQuery = $subQuery->joinWith(['tags tags'])->andWhere(['tags.id' => array_unique(explode(",", $tagIds))])->select(['pk_id']);
$find->andWhere(['in', $modelClass::primaryKey(), $inQuery]);
}
return $find->with($this->getWithRelation('list'));
}
/**
* Add new notification or update to latest primary key if exists
*
* @param string $modelClass
* @param integer $authId
* @return boolean
* @since 2.0.1
*/
protected function handleNotifications($modelClass, $authId)
{
// find the latest primary key value and store into row notifications user auth table
$pkValue = Json::encode($modelClass::findLatestPrimaryKeyValue());
$model = UserAuthNotification::find()->where(['user_id' => Yii::$app->adminuser->id, 'auth_id' => $authId])->one();
if ($model) {
$model->model_latest_pk_value = $pkValue;
$model->model_class = $modelClass::className();
return $model->save();
}
$model = new UserAuthNotification();
$model->auth_id = $authId;
$model->user_id = Yii::$app->adminuser->id;
$model->model_latest_pk_value = $pkValue;
$model->model_class = $modelClass::className();
return $model->save();
}
/**
* Append the pool where condition to a given query.
*
* If the pool identifier is not found, an exception will be thrown.
*
* @param NgRestActiveQuery|ActiveQuery $query
* @since 2.0.0
*/
private function appendPoolWhereCondition($query)
{
if ($query instanceof NgRestActiveQuery) {
$query->inPool(Yii::$app->request->get('pool'));
}
}
/**
* Returns whether the `$dataFilter` property of IndexAction should be set with the according value.
*
* @return array|boolean
* @since 1.2.2
*/
public function getDataFilter()
{
if ($this->filterSearchModelClass) {
return [
'class' => 'yii\data\ActiveDataFilter',
'searchModel' => $this->filterSearchModelClass,
];
}
return null;
}
/**
* @inheritdoc
*/
public function actions()
{
$actions = [
'index' => [ // for casual api request behavior
'class' => 'luya\admin\ngrest\base\actions\IndexAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'prepareActiveDataQuery' => [$this, 'prepareIndexQuery'],
'dataFilter' => $this->getDataFilter(),
],
'list' => [ // for ngrest list
'class' => 'luya\admin\ngrest\base\actions\IndexAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'prepareActiveDataQuery' => [$this, 'prepareListQuery'],
'dataFilter' => $this->getDataFilter(),
],
'view' => [
'class' => 'luya\admin\ngrest\base\actions\ViewAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
],
'create' => [
'class' => 'luya\admin\ngrest\base\actions\CreateAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'scenario' => $this->createScenario,
],
'update' => [
'class' => 'luya\admin\ngrest\base\actions\UpdateAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'scenario' => $this->updateScenario,
],
'delete' => [
'class' => 'luya\admin\ngrest\base\actions\DeleteAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
],
'options' => [
'class' => 'luya\admin\ngrest\base\actions\OptionsAction',
],
];
if ($this->truncateAction) {
$actions['truncate'] = [
'class' => 'luya\admin\ngrest\base\actions\TruncateAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
];
}
if ($this->enableCors) {
$actions['options']['class'] = 'luya\admin\ngrest\base\actions\OptionsAction';
}
return $actions;
}
private $_model;
/**
* Get the ngrest model object (unloaded).
*
* @return NgRestModel
* @throws InvalidConfigException
*/
public function getModel()
{
if ($this->_model === null) {
$this->_model = Yii::createObject($this->modelClass);
if (!$this->_model instanceof NgRestModelInterface) {
throw new InvalidConfigException("The modelClass '$this->modelClass' must be an instance of NgRestModelInterface.");
}
}
return $this->_model;
}
/**
* Get the Model for the API based on a given Id.
*
* If not found a NotFoundHttpException will be thrown.
*
* @param integer|string $id The id to performe the findOne() method.
* @throws NotFoundHttpException
* @return \luya\admin\ngrest\base\NgRestModel
*/
public function findModel($id)
{
$model = $this->findModelClassObject($this->modelClass, $id, 'view');
if (!$model) {
throw new NotFoundHttpException("Unable to find the Model for the given ID");
}
return $model;
}
/**
* Find the model for a given class and id.
*
* @param string $modelClass the full qualified path to the model
* @param string $id The id which is a string, for example 1 or for composite keys its 1,4
* @param string $relationContext The name of the context, which is actually the action like `search` or `index`.
* @return yii\db\ActiveRecord|boolean
*/
public function findModelClassObject($modelClass, $id, $relationContext)
{
// returns an array with the names of the primary keys
$keys = $modelClass::primaryKey();
if ((is_countable($keys) ? count($keys) : 0) > 1) {
$values = explode(',', $id);
if ((is_countable($keys) ? count($keys) : 0) === count($values)) {
return $this->findModelFromCondition($values, $keys, $modelClass, $relationContext);
}
} elseif ($id !== null) {
return $this->findModelFromCondition([$id], $keys, $modelClass, $relationContext);
}
return false;
}
/**
* This equals to the ActieRecord::findByCondition which is sadly a protected method.
*
* @param array $values An array with values for the given primary keys
* @param array $keys An array holding all primary keys
* @param string $modelClass The full qualified namespace to the model
* @param string $relationContext The name of the context like "search", "index", "list". Its acutally the action name
* @since 1.2.3
* @return yii\db\ActiveRecord
*/
protected function findModelFromCondition(array $values, array $keys, $modelClass, $relationContext)
{
$condition = array_combine($keys, $values);
// If an api user the internal find methods are used to find items.
if (!Yii::$app->adminuser->isGuest && Yii::$app->adminuser->identity->is_api_user) {
// api calls will always use the "original" find method which is based on yii2 guide the best approach to hide given data by default.
$findModelInstance = $modelClass::find();
} else {
// if its an admin user which is browsing the ui the internal ngRestFind method is used.
$findModelInstance = $modelClass::ngRestFind();
}
return $findModelInstance->andWhere($condition)->with($this->getWithRelation($relationContext))->one();
}
/**
* User Unlock
*
* Unlock this API for the current logged in user.
*
* @return void
*/
public function actionUnlock()
{
UserOnline::unlock(Yii::$app->adminuser->id);
}
/**
* Service Data
*
* @return array An array with all services information for the given ngrest model.
*/
public function actionServices()
{
$settings = [];
$apiEndpoint = $this->model->ngRestApiEndpoint();
$userSortSettings = Yii::$app->adminuser->identity->setting->get('ngrestorder.admin/'.$apiEndpoint, false);
if ($userSortSettings && is_array($userSortSettings)) {
if ($userSortSettings['sort'] == SORT_DESC) {
$order = '-'.$userSortSettings['field'];
} else {
$order = '+'.$userSortSettings['field'];
}
$settings['order'] = $order;
}
$userFilter = Yii::$app->adminuser->identity->setting->get('ngrestfilter.admin/'.$apiEndpoint, false);
if ($userFilter) {
$settings['filterName'] = $userFilter;
}
$modelClass = $this->modelClass;
// check if taggable exists, if yes return all used tags for the
if (ObjectHelper::isTraitInstanceOf($this->model, TaggableTrait::class)) {
$tags = ArrayHelper::toArray($this->model->findTags(), [
Tag::class => ['id', 'name']
]);
} else {
$tags = false;
}
$notificationMuteState = false;
$userAuthNotificationModel = UserAuthNotification::find()->where(['user_id' => Yii::$app->adminuser->id, 'auth_id' => $this->authId])->one();
if ($userAuthNotificationModel) {
$notificationMuteState = $userAuthNotificationModel->is_muted;
}
return [
'service' => $this->model->getNgRestServices(),
'_authId' => $this->authId,
'_tags' => $tags,
'_hints' => $this->model->attributeHints(),
'_settings' => $settings,
'_notifcation_mute_state' => $notificationMuteState,
'_locked' => [
'data' => UserOnline::find()
->select(['user_id', 'lock_pk', 'last_timestamp', 'firstname', 'lastname', 'admin_user.id'])
->where(['lock_table' => $modelClass::tableName()])
->joinWith('user')
->asArray()
->all(),
'userId' => Yii::$app->adminuser->id,
],
];
}
/**
* Toggle Notifications
*
* @uses integer mute Whether should be muted or not
* @return UserAuthNotification The user auth notification model. If model does not exists a new model will be created.
* @since 2.0.0
*/
public function actionToggleNotification()
{
$newMuteState = Yii::$app->request->getBodyParam('mute');
$model = UserAuthNotification::find()->where(['user_id' => Yii::$app->adminuser->id, 'auth_id' => $this->authId])->one();
if ($model) {
$model->is_muted = (int) $newMuteState;
$model->save();
} else {
$model = new UserAuthNotification();
$model->is_muted = (int) $newMuteState;
$model->auth_id = $this->authId;
$model->user_id = Yii::$app->adminuser->id;
$model->model_class = $this->modelClass;
}
return $model;
}
/**
* Search
*
* Search querys with Pagination will be handled by this action.
*
* @uses string $query The search term as post request.
* @param string $query The query to lookup the database, if query is empty a post request with `query` can be used instead.
* @return \yii\data\ActiveDataProvider
*/
public function actionSearch($query = null)
{
if (empty($query)) {
$query = Yii::$app->request->post('query');
}
$find = $this->model->ngRestFullQuerySearch($query);
$this->appendPoolWhereCondition($find);
return new ActiveDataProvider([
'query' => $find->with($this->getWithRelation('search')),
'pagination' => $this->pagination,
'sort' => [
'attributes' => $this->generateSortAttributes($this->model->getNgRestConfig()),
]
]);
}
/**
* Generate Sort attributes
*
* Generate an array of sortable attribute definitions from a ngrest config object.
*
* @param Config $config The Ngrest Config object
* @return array Returns an array with sortable attributes.
* @since 2.0.0
*/
public function generateSortAttributes(Config $config)
{
$sortAttributes = [];
foreach ($config->getPointerPlugins('list') as $plugin) {
$sortAttributes = ArrayHelper::merge($plugin->getSortField(), $sortAttributes);
}
return $sortAttributes;
}
/**
* Get Relation Data
*
* @param mixed $arrayIndex
* @param mixed $id
* @param string $modelClass The name of the model where the ngRestRelation is defined.
* @param string $query An optional query to filter the response for the given search term (since 2.0.0)
* @throws InvalidCallException
* @return \yii\data\ActiveDataProvider
*/
public function actionRelationCall($arrayIndex, $id, $modelClass, $query = null)
{
$modelClass = base64_decode($modelClass);
if (!class_exists($modelClass)) {
throw new InvalidCallException("Unable to find the given class \"$modelClass\".");
}
// `findOne((int) $id)`: (int) $id is not required as the value is safed by action param $id
$model = $modelClass::findOne($id);
if (!$model) {
throw new InvalidCallException("Unable to resolve relation call model.");
}
/** @var \luya\admin\ngrest\base\NgRestRelationInterface $relation */
$relation = $model->getNgRestRelationByIndex($arrayIndex);
if (!$relation) {
throw new InvalidCallException("Unable to find the given ng rest relation for this index value.");
}
$find = $relation->getDataProvider();
if ($find instanceof ActiveQueryInterface) {
$find->with($this->getWithRelation('relation-call'));
$this->appendPoolWhereCondition($find);
}
$targetModel = Yii::createObject(['class' => $relation->getTargetModel()]);
if ($query) {
foreach ($targetModel->getNgRestPrimaryKey() as $pkName) {
$searchQuery = $targetModel->ngRestFullQuerySearch($query)->select([$targetModel->tableName() . '.' . $pkName]);
$find->andWhere(['in', $targetModel->tableName() . '.' . $pkName, $searchQuery]);
}
}
return new ActiveDataProvider([
'query' => $find,
'pagination' => $this->pagination,
'sort' => [
'attributes' => $this->generateSortAttributes($targetModel->getNgRestConfig()),
]
]);
}
/**
* Filter
*
* @param string $filterName The filter name to apply.
* @param string $query An optional query to search inside the data.
* @throws InvalidCallException
* @return \yii\data\ActiveDataProvider
*/
public function actionFilter($filterName, $query = null)
{
$model = $this->model;
$filterName = Html::encode($filterName);
$filtersList = $model->ngRestFilters();
if (!array_key_exists($filterName, $filtersList)) {
throw new InvalidCallException("The requested filter '$filterName' does not exists in the filter list.");
}
$this->handleNotifications($this->modelClass, $this->authId);
$find = $filtersList[$filterName];
if ($query) {
foreach ($model->getNgRestPrimaryKey() as $pkName) {
$searchQuery = $model->ngRestFullQuerySearch($query)->select([$model->tableName() . '.' . $pkName]);
$find->andWhere(['in', $model->tableName() . '.' . $pkName, $searchQuery]);
}
}
$this->appendPoolWhereCondition($find);
return new ActiveDataProvider([
'query' => $find,
'pagination' => $this->pagination,
'sort' => [
'attributes' => $this->generateSortAttributes($model->getNgRestConfig()),
]
]);
}
/**
* Renders ActiveWindow Callback
*
* @return string Returns the rendered string from the callback.
*/
public function actionActiveWindowCallback()
{
$config = $this->model->getNgRestConfig();
$render = new RenderActiveWindowCallback();
$ngrest = new NgRest($config);
return $ngrest->render($render);
}
/**
* Render ActiveWindow
*
* @return array An array with content, icon, label, title and requestDate
*/
public function actionActiveWindowRender()
{
// generate ngrest active window
$render = new RenderActiveWindow();
$render->setItemId(Yii::$app->request->getBodyParam('itemId', false));
$render->setActiveWindowHash(Yii::$app->request->getBodyParam('activeWindowHash', false));
// process ngrest render view with config context
$ngrest = new NgRest($this->model->getNgRestConfig());
return [
'content' => $ngrest->render($render),
'icon' => $render->getActiveWindowObject()->getIcon(),
'label' => $render->getActiveWindowObject()->getLabel(),
'title' => $render->getActiveWindowObject()->getTitle(),
'requestDate' => time(),
];
}
/**
* Export Data.
*
* See {{luya\admin\ngrest\base\NgRestModel::ngRestExport()}} in order to configure custom export behavior for attributes.
*
* @uses integer header Whether header should be exported or not
* @uses string type The type csv oder xlsx
* @uses array attributes A list of attributes to export
* @return array An array with the key `url` which contains the download path to the file
* @throws ErrorException
*/
public function actionExport()
{
$extension = null;
$mime = null;
$header = Yii::$app->request->getBodyParam('header', 1);
$type = Yii::$app->request->getBodyParam('type');
$attributes = Yii::$app->request->getBodyParam('attributes', []);
$filter = Yii::$app->request->getBodyParam('filter', null);
$fields = ArrayHelper::getColumn($attributes, 'value');
if (!in_array($type, ['xlsx', 'csv'])) {
throw new InvalidConfigException("Invalid export type");
}
switch (strtolower($type)) {
case "csv":
$mime = 'application/csv';
$extension = 'csv';
break;
case "xlsx":
$mime = 'application/vnd.ms-excel';
$extension = 'xlsx';
break;
}
if (!empty($filter)) {
$filter = Html::encode($filter);
$filtersList = $this->model->ngRestFilters();
if (!array_key_exists($filter, $filtersList)) {
throw new InvalidCallException("The requested filter '$filter' does not exists in the filter list.");
}
$query = $filtersList[$filter]->select($fields)->with($this->getWithRelation('export'));
} else {
$query = $this->prepareListQuery()->with($this->getWithRelation('export'))->select($fields);
}
$this->appendPoolWhereCondition($query);
$exportFormatter = $this->model->ngRestExport();
if (!empty($exportFormatter)) {
$query = $this->formatExportValues($query, $exportFormatter);
foreach ($fields as $fieldIndex => $fieldValue) {
$fields[$fieldIndex] = $this->model->getAttributeLabel($fieldValue);
}
}
$tempData = ExportHelper::$type($query, $fields, (bool) $header, ['sort' => empty($exportFormatter)]);
$key = uniqid('ngrestexport', true);
if (!Yii::$app->cache->set(['download', $key], $tempData, 60 * 60)) {
throw new ErrorException("Unable to write the temporary file. Make sure the runtime folder is writeable.");
}
$menu = Yii::$app->adminmenu->getApiDetail($this->model->ngRestApiEndpoint());
$route = str_replace("/index", "/export-download", $menu['route']);
Yii::$app->session->set('tempNgRestFileName', Inflector::slug($this->model->tableName()) . '-export-'.date("Y-m-d-H-i").'.' . $extension);
Yii::$app->session->set('tempNgRestFileMime', $mime);
Yii::$app->session->set('tempNgRestFileKey', $key);
$url = Url::toRoute(['/'.$route], true);
$param = http_build_query(['key' => base64_encode($key), 'time' => time()]);
if (StringHelper::contains('?', $url)) {
$route = $url . "&" . $param;
} else {
$route = $url . "?" . $param;
}
return [
'url' => $route,
];
}
/**
* Format the ActiveQuery values based on formatter array definition see {{luya\admin\ngrest\base\NgRestModel::ngRestExport()}}.
*
* @param ActiveQuery $query
* @param array $formatter
* @return array
* @since 3.9.0
*/
private function formatExportValues(ActiveQuery $query, array $formatter)
{
Yii::$app->formatter->nullDisplay = '';
$data = [];
$rowId = 0;
foreach ($query->batch() as $batch) {
foreach ($batch as $model) {
foreach ($formatter as $formatterAttribute => $formatAs) {
if (is_callable($formatAs)) {
$data[$rowId][$model->getAttributeLabel($formatterAttribute)] = call_user_func($formatAs, $model);
} else {
$data[$rowId][$model->getAttributeLabel($formatterAttribute)] = Yii::$app->formatter->format($model[$formatterAttribute], $formatAs);
}
}
$rowId++;
}
}
return $data;
}
/**
* Active Button
*
* @param string $hash The hash from the class name.
* @param string|integer $id
* @return array|boolean
* @since 1.2.3
*/
public function actionActiveButton($hash, $id)
{
$model = $this->findModel($id);
return $model->handleNgRestActiveButton($hash);
}
/**
* Run the active selection handler for the given item
*
* @param integer $index
* @return array
* @since 4.0.0
* @throws NotFoundHttpException
*/
public function actionActiveSelection($index)
{
$class = $this->modelClass;
$items = $class::findAll(Yii::$app->request->getBodyParam('ids', [0]));
/** @var NgRestModel $model */
$model = new $class();
$activeSelections = $model->getNgRestConfig()->getActiveSelections();
if (!array_key_exists($index, $activeSelections)) {
throw new NotFoundHttpException("Unable to find the active selection handler.");
}
$object = $activeSelections[$index];
return $object->handle($items);
}
}