В этой записи будет продолжение предыдущей записи про создание собственного контроллера связанного с машинами. В этой записи, я распишу как сделать такие action’ы как просмотр всех автомобилей, изменение и удаление.
Для каждого нужно создать отдельный action. Назовем их следующим образом:
car/index
— просмотр всех автомобилейcar/update?id=n
— изменение автомобиляcar/delete?id=n
— удаление автомобиля
«n» — это уникальное ID машины
Первое — первым
Для начала, нам нужно создать метод внутри CarController
, который будет отвечать за получение данных о машине. Если она не будет найдена, то пользователю будет вылетать 404 ошибка.
Для этого я создал защищенный метод findModel()
, и её единственный аргумент — это ID машины о которой нужно получить информацию из таблицы cars
. Если машину удалось найти, то возвращается класс Car
с данными о машине (имени, модели, описании + включая все функции помощники из класса ActiveRecord
). В ином случае — выдаст пользователю 404 ошибку, используя класс NotFoundHttpException
.
Вот весь метод:
/** * Находит нужный автомобиль исходя из ключа ID. * Если машина не будет найдета, то бросает 404 HTTP ошибку. * @param integer $id Уникальное ID автомобиля. * @return Car Загруженная модель * @throws NotFoundHttpException Если не удалось найти автомобиль. */ protected function findModel($id) { if (($model = Car::findOne($id)) !== null) { return $model; } else { throw new NotFoundHttpException('Запрашиваемая страница не найдена.'); } }
Просмотр всех автомобилей (car/index)
Просмотр всех автомобилей будет по ссылке car/index
. Сейчас просматривать машины на сайте нельзя, только через phpMyAdmin и это неудобно и пора бы исправить.
Вы так же можете просматривать все машины, если вы зайдете на
http://yii.loc/car
, безindex
, потому что в Yii2index
— это action, который используется по умолчанию и его необязательно дописать в ссылку.
Action
В итоге у нас получается следующий action. С первого взгляда может показаться что все просто, но на самом деле все не так. Ниже этого кода я опишу в подробностях как он работает.
/** * Просмотр всех автомобилей * @return mixed */ public function actionIndex() { $searchModel = new CarSearch(); $dataProvider = $searchModel->search(Yii::$app->request->queryParams); return $this->render('index', [ 'searchModel' => $searchModel, 'dataProvider' => $dataProvider, ]); }
Я решил сразу сделать вывод машин с возможностью поиска и пагинацией.
В actionIndex()
мы будем использовать класс поиска CarSearch
и объявим его в виде $searchModel
. Далее используем метод search()
, который принимает текущие параметры $_GET
запроса (чтобы метод понимал, что именно нужно искать). Чуть ниже будет подробное описание этого этапа.
И конечно же в конце action важно указать название шаблона (index
) и добавить нужные параметры для отображения ($searchModel
и $dataProvider
).
$searchModel
— это модель для проверки полей, точно такая же какLoginForm
, только тут немного другие методы + добавлен поиск$dataProvider
— это список машин
Давайте теперь создадим класс CarSearch
в models/search
, то есть внутри models
вам нужно создать новую папку search
.
Мы не добавляем класс
CarSearch
внутри папкиmodels
, потому что лучше сразу разграничить разные классы по определенным папкам. В дальнейшем, у вас может быть 10 контроллеров и у каждого будет класс для поиска. Поэтому если вы добавите их все в папкуmodels
— то будет каша, которую вам потом нужно будет разгребать. Поверьте, такая головная боль вам не нужна.
Код для класса CarSearch
:
<?php namespace app\models\search; use yii\base\Model; use yii\data\ActiveDataProvider; use app\models\Car; /** * CarSearch представляет модель поиска данных от `app\models\Car`. */ class CarSearch extends Car { public function rules() { return [ [['id', 'created_at', 'updated_at'], 'integer'], [['name', 'model', 'description'], 'safe'], ]; } public function scenarios() { return Model::scenarios(); } /** * Создаем data provider, чтобы поиск работал как нужно * * @param array $params * * @return ActiveDataProvider */ public function search($params) { $query = Car::find(); $dataProvider = new ActiveDataProvider([ 'query' => $query, ]); $this->load($params); if (!$this->validate()) { return $dataProvider; } $query->andFilterWhere([ 'id' => $this->id ]); $query->andFilterWhere(['like', 'name', $this->name]) ->andFilterWhere(['like', 'model', $this->model]) ->andFilterWhere(['like', 'description', $this->description]); return $dataProvider; } }
Как видно в коде выше, CarSearch
заимствует все методы из Car
, тем самым он тоже может управлять данными из таблицы cars
.
В CarSearch
вы перезаписываем метод rules()
на другие, потому что тут мы указываем поля, которые могут быть использованы для поиска с помощью валидатора safe
.
Далее мы используем метод search()
для того, чтобы управлять поиском используя класс ActiveDataProvider
. Ваша основная задача — это составить SQL запрос и запустить в ActiveDataProvider
— все остальная магия происходит внутри класса. Строчка $this->load($params)
подгружает параметры поиска в ActiveRecord
класс и тем самым он знает, что вы пытаетесь найти.
В остальном, в методе search()
вы просто делаете свои правила по поиску в базе, которые состоят из методов andFilterWhere()
(дословный перевод — «и фильтруй где». В примере выше, этот метод используется для фильтрации данных, первым аргументов тип фильтрации, в данном случае — это like
(это команда «LIKE» в SQL), далее вы указываете по какому полю вы хотите производить поиск (например, name
) и какой атрибут будет использоваться для поиска данных (например, $this->name
).
Шаблон
Теперь самое важное — шаблон для car/index
. Тут мы будем использовать виджет GridView
, который выводит данные в виде таблицы и все что вам нужно сделать — это указать данные, которые вы хотите вывести, а они у нас в $dataProvider
, который мы отправляем из actionIndex()
.
Внутри GridView::widget
, вам нужно указать настройки для виджета в виде массива. Вариантов настроек у него много, но я распишу лишь те, которые присутствуют в примере, ибо это заслуживает отдельный пост, а не просто параграф.
Описание настроек GridView
:
dataProvider
— указываем сформированный SQL запрос изCarSearch
, параметр отправляется черезactionIndex()
filterModel
— указываете модель, которая используется для фильтрации поиска, опять жеCarSearch
, параметр отправляется черезactionIndex()
columns
— указываете список колонок из базы, которые вы хотите вывести['class' => 'yii\grid\ActionColumn']
в спискеcolumns
могут быть внутренние массивы, один из них — этоActionColumn
, который отображает колонку действиями: редактирование, просмотр и удаление. По умолчания они отображаются как иконки из Bootstrap, внутри ссылки<a></a>
.
Если вы обратите внимательнее, то в коде ниже, еще есть <?php Pjax::begin(); ?>
и <?php Pjax::end(); ?>
. Этот виджет используется для того, чтобы обновлять контент между ними без перезагрузки страницы. То есть когда у вас будет много машин и вы будите переключать между страницами, используя пагинацию, то сама страница перезагружаться не будет, только талица.
<?php use yii\helpers\Html; use yii\grid\GridView; use yii\widgets\Pjax; /* @var $this yii\web\View */ /* @var $searchModel app\models\search\CarSearch */ /* @var $dataProvider yii\data\ActiveDataProvider */ $this->title = 'Все машины'; $this->params['breadcrumbs'][] = $this->title; ?> <div class="car-index"> <h1><?= Html::encode($this->title) ?></h1> <?php echo $this->render('_search', ['model' => $searchModel]); ?> <p> <?= Html::a('Создать новую', ['create'], ['class' => 'btn btn-success']) ?> </p> <?php Pjax::begin(); ?> <?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', 'name', 'model', 'description', 'created_at', ['class' => 'yii\grid\ActionColumn'], ], ]); ?> <?php Pjax::end(); ?> </div>
Теперь если вы зайдете на car/add
, успешно добавите новый автомобиль и потом перейдете на car/index
, то в списке автомобилей должна быть ваша машина.
Выглядит страница следующим образом:
Выглядит неплохо, но давайте подправим дату, чтобы была читабельной для человеческого мозга используя форматтер даты, потому что сейчас она в UNIX формате.
Замените в шаблоне строчку:
... 'created_at', ...
На это:
... [ 'attribute' => 'created_at', 'content' => function ($car) { return Yii::$app->formatter->asDate($car->created_at); } ], ...
Мы заменили обычную строчку, на массив и внутри, мы указываем, что мы все равно изменяем атрибут — created_at
и используем callable
(самовызывающиеся) функцию content
, которая как аргумент добавляет текущую машину из списка (то есть каждую машину) и тем самым внутри, мы просто возвращаем Yii::$app->formatter
и указываем, что мы хотим изменить формат даты методом asDate()
основываясь на настройках из config/web.php
(строчка 'dateFormat' => 'dd.MM.yyyy'
). Тем самым, если вы измените формат dd.mm.yyy
в конфиге на что-то еще, то он так же измениться везде, где вы использовали метод asDate()
— в этом и удобство :)
После изменений, список машин должен выглядеть следующим образом:
Просмотр одной машины
Важно просматривать не только все машины, но и по одной тоже.
Наш action будет выглядеть следующим образом:
/** * Просмотр определенного автомобиля * @param integer $id * @return mixed */ public function actionView($id) { return $this->render('view', [ 'model' => $this->findModel($id), ]); }
Все что у вас там есть — это мы ищем машину, использую метод findModel()
по определенному $id
и если находим, то перекидываем данные в view.php
, который занимается выводом данных о ней.
Шаблон
Шаблон для вывода машины выглядит следующим образом:
<?php use yii\helpers\Html; use yii\widgets\DetailView; /* @var $this yii\web\View */ /* @var $model app\models\Car */ $this->title = $model->name; $this->params['breadcrumbs'][] = ['label' => 'Cars', 'url' => ['index']]; $this->params['breadcrumbs'][] = $this->title; ?> <div class="car-view"> <h1><?= Html::encode($this->title) ?></h1> <p> <?= Html::a('Изменить', ['update', 'id' => $model->id], ['class' => 'btn btn-primary']) ?> <?= Html::a('Удалить', ['delete', 'id' => $model->id], [ 'class' => 'btn btn-danger', 'data' => [ 'confirm' => 'Вы уверены, что хотите удалить эту машину?', 'method' => 'post', ], ]) ?> </p> <?= DetailView::widget([ 'model' => $model, 'attributes' => [ 'id', 'name', 'model', 'description', 'created_at', 'updated_at', ], ]) ?> </div>
И давайте так же заменим атрибуты created_at
и updated_at
на использование форматтера. Поэтому заменяем это:
... 'created_at', 'updated_at', ...
На это:
... [ 'attribute' => 'created_at', 'value' => function ($car) { return Yii::$app->formatter->asDate($car->created_at); } ], [ 'attribute' => 'created_at', 'value' => function ($car) { return Yii::$app->formatter->asDate($car->created_at); } ], ...
Обратите внимание на value
вместо content
, потому что используется виджет DetailView
— это его разница между изменениями в формате отображения данных.
В виджете
GridView
используется —content
, а в DetailView используется —value
.
Изменение автомобиля
Это хорошо, что мы сделали возможность добавления машин, но так же важно добавить возможность их изменения.
Суть этого action простая:
- Принимает один аргумент — это ID машины в виде
$id
- После чего мы пытаемся получить информацию об этом машине используя защищенный метод
findModel()
, в случае если машины нет — выдаст 404. - Если все окей, машина нашлась, то выводит страницу с редактированием, часть с
$this->render()
- Если вы пытаетесь сохранить форму, то работает часть с
$model->load()
и$model->save()
/** * Изменение определенного автомобиля. * @param integer $id Уникальное ID автомобиля. * @return string|\yii\web\Response */ public function actionUpdate($id) { $model = $this->findModel($id); if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['view', 'id' => $model->id]); } else { return $this->render('update', [ 'model' => $model, ]); } }
Шаблон
Шаблон для обновления машины выглядит следующим образом:
<?php use yii\helpers\Html; /* @var $this yii\web\View */ /* @var $model app\models\Car */ $this->title = 'Изменение машины: ' . $model->name; $this->params['breadcrumbs'][] = ['label' => 'Машины', 'url' => ['index']]; $this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['view', 'id' => $model->id]]; $this->params['breadcrumbs'][] = 'Изменить'; ?> <div class="car-update"> <h1><?= Html::encode($this->title) ?></h1> <?= $this->render('_form', [ 'model' => $model, ]) ?> </div>
Если вы обратили внимание, то тут нет класса ActiveForm
с полями формы, потому что это я вырезал и добавил в отдельный файл _form.php
. Он будет использоваться для update.php
и create.php
, потому что эти файлы делают одну и ту же задачу (почти), разница лишь в двух моментах:
- на странице изменения поля должны быть заполнены
- текст кнопки формы различается в зависимости от файла,
create.php
— «Создать»,update.php
— «Обновить»
Код для _form.php
:
<?php use yii\helpers\Html; use yii\widgets\ActiveForm; /* @var $this yii\web\View */ /* @var $model app\models\Car */ /* @var $form yii\widgets\ActiveForm */ ?> <div class="car-form"> <?php $form = ActiveForm::begin(); ?> <?= $form->field($model, 'name')->textInput(['maxlength' => true]) ?> <?= $form->field($model, 'model')->textInput(['maxlength' => true]) ?> <?= $form->field($model, 'description')->textarea(['maxlength' => true]) ?> <div class="form-group"> <?= Html::submitButton($model->isNewRecord ? 'Создать' : 'Обновить', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> </div> <?php ActiveForm::end(); ?> </div>
Теперь я могу также использовать метод render()
с нужными параметрами ($model
) для того, чтобы выводить шаблон (_form.php
) внутри шаблона (update.php
).
Выглядит страница следующим образом:
Удаление автомобиля
Этот action особенный, потому что у него нет шаблона, он выполняет функцию удаления и редиректа (перенаправления пользователя) на другую страницу. В коде ниже, после успешного удаления, пользователь будет перенаправлен на car/index
, на страницу всех машин.
/** * Удалить существующий автомобиль. * Если удаление будет успешным, то пользователя перекинет на главную страницу (index). * @param integer $id Уникальное ID машины. * @return mixed */ public function actionDelete($id) { $this->findModel($id)->delete(); return $this->redirect(['index']); }
Подправим старый код под новый
Заменим в CarController
это:
public function actionCreate() { $model = new Car(); if( $model->load(Yii::$app->request->post()) && $model->validate() ) { if( $model->save() ) { return $this->refresh(); } } return $this->render('create', ['model' => $model]); }
На это:
/** * Создание нового автомобиля. * Если удаление будет успешным, то пользователя перекинет на главную страницу (view). * @return mixed */ public function actionCreate() { $model = new Car(); if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['view', 'id' => $model->id]); } else { return $this->render('create', [ 'model' => $model, ]); } }
Заменить ВСЁ внутри файла views/car/create.php
на:
<?php /* @var $this yii\web\View */ /* @var $form yii\bootstrap\ActiveForm */ /* @var $model app\models\Car */ use yii\helpers\Html; use yii\bootstrap\ActiveForm; $this->title = 'Добавить новую машину'; $this->params['breadcrumbs'][] = $this->title; ?> <div class="site-login"> <h1><?= Html::encode($this->title) ?></h1> <?= $this->render('_form', [ 'model' => $model, ]) ?> </div>
Что изменилось?
- Внутри
actionCreate()
мы заменилиvalidate()
наsave()
, так какvalidate()
нам не нужно вызывать — он сам вызывается при вызове методаsave()
. И еслиsave()
вернетfalse
, то добавление новой машины в базу, в любом случае, не произойдет. - Внутри
create.php
мы удалили классActiveForm
с полями формы и использовали$this->render()
метод, который использует шаблон_form.php
для отображения её отображения. Таким образом, мы делаем разработку приложения более модульной и гибкой.
Читать далее — Yii2 basic: Регистрация и авторизация через базу, меняем User класс