Дополнительные данные для разных действий

Всё что касается построения API
Ответить
Аватара пользователя
alexk984
Сообщения: 433
Зарегистрирован: 2010.10.21, 15:03
Контактная информация:

Дополнительные данные для разных действий

Сообщение alexk984 »

Привет, уже спрашивал но может что-нибудь новое прояснится. Для вывода данных из связанных таблиц я использую такой способ:

Код: Выделить всё

public function fields() {
     $fields = parent::fields();

     $fields['posts'] = function (User $model)
     {
         return $model->posts;
     };

     return $fields;
} 
Но возникает проблема когда в одном экшне мне нужно выводить эти данные, а в другом нет, приходится добавлять условие в модель

Код: Выделить всё

if (Yii::$app->controller->action->id == 'index') ... 
Это очень неудобно, можно ли решить проблему по-другому?
shkarbatov
Сообщения: 423
Зарегистрирован: 2012.12.10, 14:19
Откуда: Россия

Re: Дополнительные данные для разных действий

Сообщение shkarbatov »

Посмотрите тут пример работы со связанными данными:
https://github.com/yiisoft/yii2/blob/ma ... и-данными-

Сделайте метод, который будет возвращать нужные Вам данные, в модели, а потом дергайте его из контроллера. По хорошему, модель не должна ничего знать о контроллере.
Аватара пользователя
alexk984
Сообщения: 433
Зарегистрирован: 2010.10.21, 15:03
Контактная информация:

Re: Дополнительные данные для разных действий

Сообщение alexk984 »

Видимо я не очень понятно выразился дело не в связанных данных, возвращаться могут любые данные, но проблема в том что в одном экшне они нужны а в другом не нужны, а метод fields он один на всех. Например есть 2 экшна

Код: Выделить всё

public function actionIndex()
{
    return User::find()->all();
}

public function actionView($id)
{
    return User::find()->where(['type' => $id])->all();
}
В actionView я хочу еще вывести дополнительные данные, к примеру я их каким-то хитрым образом выбираю, есть метод в модели который это делает. Придется писать такой код:

Код: Выделить всё

public function fields() {
     $fields = parent::fields();

     if (Yii::$app->controller->action->id == 'view')
     {
          $fields['stats'] = function (User $model)
          {
              return $model->getStats();
          };
     }

     return $fields;
} 
Как сделать это иначе?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Дополнительные данные для разных действий

Сообщение zelenin »

вам верно сказали: модель не должна знать ничего о контроллере. Контроллер должен наоборот забрать необходимые данные из модели.
Аватара пользователя
alexk984
Сообщения: 433
Зарегистрирован: 2010.10.21, 15:03
Контактная информация:

Re: Дополнительные данные для разных действий

Сообщение alexk984 »

Так я спрашиваю как это сделать
shkarbatov
Сообщения: 423
Зарегистрирован: 2012.12.10, 14:19
Откуда: Россия

Re: Дополнительные данные для разных действий

Сообщение shkarbatov »

Напишите пожалуйста реальный пример, трудно показывать такое на пальцах. И мне немного не понятна причина, по которой надо было бы так писать в fields(), чего именно Вы хотели этим добиться?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Дополнительные данные для разных действий

Сообщение zelenin »

shkarbatov писал(а):Напишите пожалуйста реальный пример, трудно показывать такое на пальцах. И мне немного не понятна причина, по которой надо было бы так писать в fields(), чего именно Вы хотели этим добиться?
очевидно хочет в разных экшнах получить разный выхлоп
shkarbatov
Сообщения: 423
Зарегистрирован: 2012.12.10, 14:19
Откуда: Россия

Re: Дополнительные данные для разных действий

Сообщение shkarbatov »

Просто тогда не понятно, почему из экшена просто не сделать обычный запрос в БД с нужными данными, посредством модели?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Дополнительные данные для разных действий

Сообщение zelenin »

shkarbatov писал(а):Просто тогда не понятно, почему из экшена просто не сделать обычный запрос в БД с нужными данными, посредством модели?
потому что он пишет rest api на основе встроенного rest-компонента, который юзает fields для сборки респонса.
Аватара пользователя
alexk984
Сообщения: 433
Зарегистрирован: 2010.10.21, 15:03
Контактная информация:

Re: Дополнительные данные для разных действий

Сообщение alexk984 »

Реальный пример я полностью написал. zelenin прав, я пишу Api используя Rest-компонент, который заботится о пагинации например в случае если экшн возвращает DataProvider и еще много о чем. Именно поэтому я не могу просто вызвать какой-то метод из контроллера, так как контроллер возвращает массив или одну модель а не данные.
Как вариант можно использовать разные модели для разных экшенов и наследовать от общей модели ApiUser, но получается мы плодим модели, что тоже не очень приятно, впрочем я так понимаю что лучшего решения нет.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Дополнительные данные для разных действий

Сообщение zelenin »

alexk984 писал(а):Реальный пример я полностью написал. zelenin прав, я пишу Api используя Rest-компонент, который заботится о пагинации например в случае если экшн возвращает DataProvider и еще много о чем. Именно поэтому я не могу просто вызвать какой-то метод из контроллера, так как контроллер возвращает массив или одну модель а не данные.
Как вариант можно использовать разные модели для разных экшенов и наследовать от общей модели ApiUser, но получается мы плодим модели, что тоже не очень приятно, впрочем я так понимаю что лучшего решения нет.
вам нужно переписать Serializer под себя, и в контроллере его переназначать соответственно. Думаю, дело 5 минут.
https://github.com/yiisoft/yii2/blob/ma ... r.php#L245
Аватара пользователя
alexk984
Сообщения: 433
Зарегистрирован: 2010.10.21, 15:03
Контактная информация:

Re: Дополнительные данные для разных действий

Сообщение alexk984 »

zelenin писал(а):вам нужно переписать Serializer под себя, и в контроллере его переназначать соответственно. Думаю, дело 5 минут.
https://github.com/yiisoft/yii2/blob/ma ... r.php#L245
Не понял, как переписать и переназначать?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Дополнительные данные для разных действий

Сообщение zelenin »

alexk984 писал(а):
zelenin писал(а):вам нужно переписать Serializer под себя, и в контроллере его переназначать соответственно. Думаю, дело 5 минут.
https://github.com/yiisoft/yii2/blob/ma ... r.php#L245
Не понял, как переписать и переназначать?
я кинул ссылку, где преедается набор полей для вывода - вы можете там поставить условие, передавая свой набор полей.
Аватара пользователя
alexk984
Сообщения: 433
Зарегистрирован: 2010.10.21, 15:03
Контактная информация:

Re: Дополнительные данные для разных действий

Сообщение alexk984 »

А то есть я буду в экшене expand задавать чего сейчас нельзя сделать. Да, это мне кажется в самом фреймворке можно было бы сделать
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Дополнительные данные для разных действий

Сообщение zelenin »

alexk984 писал(а):А то есть я буду в экшене expand задавать чего сейчас нельзя сделать. Да, это мне кажется в самом фреймворке можно было бы сделать
это неправильно
Аватара пользователя
pr_o
Сообщения: 65
Зарегистрирован: 2010.01.19, 02:20

Re: Дополнительные данные для разных действий

Сообщение pr_o »

Подниму тему, АКТУАЛЬНО...
Мое решение: можно установить кастомное значение для свойства модели $scenario и создать по нему условие в методе fields.
В $query dataProvider-а кастомизируем зависимости:

Код: Выделить всё

$query->joinWith(['relation' => function ($query) {
        $this->relation->scenario = 'fieldsSimple';
}]); 
В принципе должно сработать, буду сегодня экспериментировать, но оптимального решения я не нашел! Увы :-(

P.S. Проверил пример выше - НЕ РАБОТАЕТ, напишу рабочее решение в след. сообщении
Аватара пользователя
pr_o
Сообщения: 65
Зарегистрирован: 2010.01.19, 02:20

Re: Дополнительные данные для разных действий

Сообщение pr_o »

Мне УДАЛОСЬ ИЗМЕНИТЬ СПИСОК ПОЛЕЙ в результате вывода dataProvider (в т.ч. и поля для вложенных объектов, relations)
И так, как я писал в пред. сообщении:
pr_o писал(а):Мое решение: можно установить кастомное значение для свойства модели $scenario и создать по нему условие в методе fields.[/code]
Это решение оставим и для примера создадим пару моделей:

Код: Выделить всё

namespace app\models;

/**
 * Модель пользователя
 */
class User extends \yii\db\ActiveRecord {
    
    const SCENARIO_LIGHT_FIELDS = 'lightFields';
    
    ..
    
    public function scenarios()
    {
        return \yii\helpers\ArrayHelper::merge(parent::scenarios(), [
            // Пустой массив означает, что отработают все rules данной модели при валидации для этого сценария
            // см. валидаторы..
            self::SCENARIO_LIGHT_FIELDS => [],
        ]);
    }
    
    ..
    
    public function fields()
    {
        $fields = ["id", "username", "active", "profile", "created_at", "updated_at"];
        if (self::SCENARIO_LIGHT_FIELDS === $this->scenario) {
            // ТОЛЬКО поля "id" и "profile" выведутся при сценарии "lightFields"
            // При том, что поле "profile" - это relation hasOne к модели Profile
            $fields =  array_intersect($fields, ['id', 'profile']);
        }
        return $fields;
    }
    
    ..

    public function getProfile()
    {
        return $this->hasOne(Profile::className(), ['user_id' => 'id']);
    }

    ..
}

Код: Выделить всё

namespace app\models;

/**
 * Модель профиля пользователя
 */
class Profile extends \yii\db\ActiveRecord {
    
    const SCENARIO_LIGHT_FIELDS = 'lightFields';
    
    ..
    
    public function scenarios()
    {
        return \yii\helpers\ArrayHelper::merge(parent::scenarios(), [
            self::SCENARIO_LIGHT_FIELDS => [],
        ]);
    }
    
    ..
    
    public function fields()
    {
        $fields = ["id", "user_id", "first_name", "last_name", "created_at", "updated_at"];
        if (self::SCENARIO_LIGHT_FIELDS === $this->scenario) {
            // ТОЛЬКО поля "first_name" и "last_name" выведутся при сценарии "lightFields"
            $fields =  array_intersect($fields, ['first_name', 'last_name']);
        }
        return $fields;
    }
    
    ..
}
 
Заметим, что в классе модели свойство $scenario уже предопределено.

Теперь переходим к самой интересной части (rest и вывод данных в json).
И так, по порядку, что за чем вызывается от выбора данных до вывода в response клиенту:
- ..
- yii\rest\ActiveController определяет, кто отвечает за index action.
- yii\rest\IndexAction По умолчанию отвечает за действие контроллера, указанное выше.
- Сам IndexAction в итоге возвращает dataProvider
- Далее dataProvider попадает в yii\rest\Serializer
- И только потом вступает в силу response formatter
- ..

Для решения поставленной задачи нам нужно унаследовать класс yii\rest\IndexAction и вклиниться в метод prepareDataProvider().
В dataProvider-ре есть замечательный метод getModels(), который выбирает из БД нужные ему данные и создает по ним модели.
Нам всего-лишь надо будет перебрать все модели (в т.ч. и relations) и установить для каждой из них свойство $scenario, что бы сработало для них условие в методе fields. Следующие примеры кода это все демонстрируют:

Контроллер:

Код: Выделить всё

namespace app\controllers;

use app\models\User;
use app\rest\user\IndexAction;

controller UserController extends \yii\rest\ActiveController {

    public $modelClass = User::class;

    public function actions()
    {
        return \yii\helpers\ArrayHelper::merge(parent::actions(), [
            'index' => [
                'class' => IndexAction::class
            ],
        ]); 
    }
}
Действие контроллера (action) в отдельном классе:

Код: Выделить всё

namespace app\rest\user;

class IndexAction extends \yii\rest\IndexAction
{
    /**
     * Prepares the data provider that should return the requested collection of the models.
     * @return ActiveDataProvider
     */
    protected function prepareDataProvider()
    {
        /** @var yii\data\ActiveDataProvider */
        $dataProvider = parent::prepareDataProvider();
        
        /** @var yii\db\ActiveQuery */
        $query = $dataProvider->query;
        // Кастомизируем запрос, ЕСЛИ НУЖНО (чаще всего - да)
        // $query->andWhere(['=', 'active', 1]);
        
        // Теперь пришло время расставить точки над "и" и установить сценарии для моделей
        array_map(function ($model) {
            $model->setScenario($model::SCENARIO_LIGHT_FIELDS);

            if ($model->profile) {
                $model->profile->setScenario($model->profile::SCENARIO_LIGHT_FIELDS);
            }

            // Следующий кусок кода я комментирую, АВОСЬ ПРИГОДИТСЯ:
            /*
            array_map(function ($model) {
                $model->setScenario($this->scenarion);

                if ($model->from) {
                    $model->from->setScenario($this->scenarion);
                }

            }, $model->chatMessages);
            */

        }, $dataProvider->getModels());

        return $dataProvider;
    }
}
Ну и для полного счастья привожу конфиг компонента urlManager-а:

Код: Выделить всё

return [
    ..
    'components' => [
        'request' => [
            'enableCookieValidation' => false,
            'enableCsrfValidation' => false,
            'parsers' => [
                'application/json' => 'yii\web\JsonParser',
            ],
        ],
        'urlManager' => [
            'enablePrettyUrl' => true,
            'enableStrictParsing' => true,
            'showScriptName' => false,
            'rules' => [
                ['class' => 'yii\rest\UrlRule', 'controller' => 'user'],
            ],
        ],
    ],
    ..
];
Ссылка для результата {ВАШ_YII2_САЙТ}/{baseUrl}/users

Вот и все, ребята! Удачи!
Ответить