Yii2 basic: Просмотр, изменение и удаление автомобилей

· Yii2 basic · 16 мин чтения

В этой записи будет продолжение предыдущей записи про создание собственного контроллера связанного с машинами. В этой записи, я распишу как сделать такие 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, потому что в Yii2 index — это 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, то в списке автомобилей должна быть ваша машина.

Выглядит страница следующим образом:

Yii2 basic: страница со всеми машинами

Выглядит неплохо, но давайте подправим дату, чтобы была читабельной для человеческого мозга используя форматтер даты, потому что сейчас она в 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() — в этом и удобство :)

После изменений, список машин должен выглядеть следующим образом:

Yii2 basic formatter, изменили формат даты

Просмотр одной машины

Важно просматривать не только все машины, но и по одной тоже.

Наш 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).

Выглядит страница следующим образом:

Yii2 basic: страница изменения автомобиля

Удаление автомобиля

Этот 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 класс