Создание простого CRUD-приложения с помощью Yii2

Содержание

Disclaimer

Этот урок поможет вам познакомиться с Yii2. В данный момент Yii2 абсолютно не готов для продакшена. Я не рекомендую использовать его в рабочих проектах.

Начнём

Сегодня авторы Yii Framework анонсировали публичное превью Yii2. Между Yii1 и Yii2 довольно много изменений.

Этот урок описывает создание простого блога на Yii2. В процессе мы будем скачивать и устанавливать Yii2, создавать базовое приложение, подключаться к базе данных и описывать логику создания, обновления, чтения и удаления постов.

Для продолжения вам потребуется:

  • Вебсервер, например Apache или Nginx. Я буду использовать Nginx. Использование Apache будет отличаться незначительно, поэтому не волнуйтесь если у вас нет поблизости сервера с Nginx.
  • Север базы данных для нашего приложения. Я выбрал MySQL 5.5
  • Базовые знания PHP. Я постараюсь объяснять всё так просто, как это возможно, но чем лучше вы знаете PHP, тем проще вам будет двигаться дальше.
  • Базовые знания Yii или MVC. Если у вас нет никакого опыта работы с MVC, то я рекомендую вам прочесть основы MVC. Вы можете прочитать этот урок и без знания MVC, но вам будет гораздо легче понимать происходящее, разбираюсь в теме MVC.

Вперёд!
Я буду предполагать, что у вас уже есть настроенный вебсервер. В уроке я буду использовать следующие пути и адреса:

  • /var/www/yii2 в качестве DocumentRoot
  • yii2.erianna.com в качестве адреса хостинга

Также, в конце урока вы сможете посмотреть пример приложения, которые мы с вами сделаем на yii2.erianna.com.

Скачиваем Yii2

Получить копию Yii2 с Github можно либо клонировав репозиторий, либо скачав архив.

git clone [email protected]:yiisoft/yii2.git /dir/to/yii2

либо

wget https://github.com/yiisoft/yii2/archive/master.zip
unzip master.zip /dir/to/yii2

После распаковки Yii2, перейдите в папку /dir/to/yii2/framework

cd /dir/to/yii2/framework

И запустите следующую команду для создания базового веб-приложения, ответив Yes на первый вопрос.

php yiic.php app/create /var/www/yii2
yes

Это эквивалент команде создания веб-приложения в Yii 1.x. Теперь перейдите в /var/www/yii2. Вы увидите одну папку и один файл.

$ ls -l
total 8
-rwxrwxrwx 1 user www-data  265 May  4 09:30 index.php
drwxrwsr-x 5 user www-data 4096 May  4 09:07 protected

Перед тем, как запустить наш сайт, мы должны внести некоторые модификации в файл index.php. На мой взгляд, там есть несколько спорных решений. Надеюсь, они будут исправлены до финального релиза Yii2, чтобы сделать его более дружественным в установке.

Измените index.php следующим образом:

<?php
define('YII_DEBUG', true);

// Change this to your actual Yii path
require '/path/to/yii2/framework/yii.php';

// Change __DIR__ to __FILE__ so we can load our config file
$config = require dirname(__FILE__).'/protected/config/main.php';
$config['basePath'] = dirname(__FILE__).'/protected';

$app = new \yii\web\Application($config);
$app->run();

Посмотрим на то, что мы изменили:

// Change this to your actual Yii path
require '/path/to/yii2/framework/yii.php';

Во-первых, мы должны изменить путь к файлу framework/yii.php. По умолчанию считается, что он лежит в той же директории. Возможно, так и бывает, но нам нужно указать точный путь к Yii2.

$config = require dirname(__FILE__).'/protected/config/main.php';
$config['basePath'] = dirname(__FILE__).'/protected';

Во-вторых, мы обновляем путь к конфигурационному файлу, чтобы он использовал __FILE__ вместо __DIR__. Это необходимо, чтобы бы ваше приложение могло запуститься.

Перед тем как мы продолжим, важно отметить кое-что новое в Yii — Namespaces

$app = new \yii\web\Application($config);

Смысл пространств имён в инкапсуляции кода в логические единицы, чтобы избежать коллизий между различными кодовыми базами. Допустим, у вас есть два класса, оба называются Foo и оба имеют метод Bar. Если они будут располагаться в различных пространствах имён, то вы сможете вызывать их независимо друг от друга без каких-либо коллизий.

$foo = new \namespace\Foo;
$foo2 = new \namespace2\Foo;

Пространства имён это простой способ избежать коллизий в коде. Я рекомендую вам прочитать о них, так как Yii2 будет полностью построен на этом принципе.

Теперь вы создали первое приложение! Перейдите на адрес, по которому у вас расположен yii2 и вы увидите следующую страницу.

c4ca4238a0b923820dcc509a6f75849b
Ваше первое приложение на Yii2!

В отличии от Yii 1.x, базовое приложение Yii2 не такое восхищающее. Давайте научим его делать немного больше.

Первым делом откроем файл /protected/views/layout/main.php и заменим его содержимое:

<?php use yii\helpers\Html as Html; ?>
<!doctype html>
<html lang="<?php \Yii::$app->language?>">
    <head>
        <meta charset="utf-8" />
        <title><?php echo Html::encode(\Yii::$app->name); ?></title>
        <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
        <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
    </head>
    <body>
        <div class="container">
            <div class="navbar navbar-inverse">
                <div class="container">
                    <div class="navbar-inner">
                        <a class="brand" href="/"><?php echo Html::encode(\Yii::$app->name); ?></a>
                    </div>
                </div>
            </div>
            <div class="content">
                <?php echo $content?>
            </div>
        </div>
    </body>
</html>

Теперь обновите страницу. Видите? Разве не всё становится лучше с Twitter Bootstrap? Опять же, не так и много изменилось между Yii1 и Yii2. У вас по-прежнему есть переменная $content для отображения контента во view. Однако, Yii::app() заменили на Yii::$app. И напомню ещё раз — всё в Yii2 разделено по пространствам имён, поэтому важно запомнить обращаться ко всему по их пространству имён, а не просто вызывать «сырые» классы.

Теперь займёмся настоящим кодингом!

Подключаемся к базе данных

Для этого приложения у нас будет лишь простая таблица Posts, в которой мы будем хранить посты нашего блога.

Создаём таблицу в базе данных

Войдите в MySQL и создайте пользователя и базу данных с именем yii2. Затем выполните следующий запрос для обновления структуры:

DROP TABLE IF EXISTS `posts`;
CREATE TABLE IF NOT EXISTS `posts` (
  `id` int(11) NOT NULL,
  `title` varchar(255) NOT NULL,
  `content` text NOT NULL,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ALTER TABLE `posts` ADD PRIMARY KEY(`id`);

INSERT INTO `yii2`.`posts` (`id`, `title`, `content`, `created`, `updated`) VALUES ('1', 'Example Title', 'New Post', NOW(), NOW());

 

Обновляем конфиг

Перейдите в папку /var/www/yii2/protected/ и откройте файл config.php в любимом редакторе. Замените всё его содержимое на это:

<?php
return array(
'id' => 'webapp',
'name' => 'My Web Application',

'components' => array(
        // uncomment the following to use a MySQL database
        'db' => array(
                'class' => 'yii\db\Connection',
                'dsn' => 'mysql:host=localhost;dbname=yii2',
                'username' => 'yii2', 
                'password' => '<password>',
                ),
                'cache' => array(
                        'class' => 'yii\caching\DummyCache',
                ),
        ),
);

Если вы знакомы с Yii, то заметите сильное улучшение относительно тех монструозных конфигов, которые генерировал Yii1. Несмотря на то, что структура осталась прежней, это всё, что нам нужно для подключения к базе данных.

Создаём модель Post

Создайте новую папку models в папке protected, а затем создайте в ней файл Post.php со следующим кодом.

<?php
namespace app\models;
class Post extends \yii\db\ActiveRecord
{
    /**
     * Returns the static model of the specified AR class.
     * @param string $className active record class name.
     * @return Comments the static model class
     */
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }

    /**
     * @return string the associated database table name
     */
    public static function tableName()
    {
        return 'posts';
    }

    /**
     * @return array primary key of the table
     **/     
    public static function primaryKey()
    {
        return array('id');
    }

    /**
     * @return array customized attribute labels (name=>label)
     */
    public function attributeLabels()
    {
        return array(
            'id' => 'ID',
            'title' => 'Title',
            'content' => 'Content',
            'created' => 'Created',
            'updated' => 'Updated',
        );
    }
}

Если вы знакомы с Yii1, единственное что изменилось в ActiveRecord (по крайней мере в этом примере), это то, что функции primaryKey и tableName теперь являются статическими методами. Всё остальное в основном осталось прежним. По большей части, ActiveRecord осталась нетронутой.

Самой важной частью класса является включение пространства имён app\models. Это говорит Yii как мы можем ссылаться на этот файл.

В отличии от Yii1, где вы можете просто вызвать имя класса, Yii2 использует другой тип автозагрузки файлов, который требует от вас точного указания того, что вы собираетесь использовать. Это может несколько замедлить разработку (Попытки запомнить всегда подключать \yii\framework\web\Html вместо простого вызова CHtml могут утомить), но в тоже время сделает Yii2 значительно быстрее по той причине, что автозагрузчик теперь не должен искать по всему фреймворку, чтобы загрузить какой-то один класс. По крайней мере, в теории.

CRUD!

После того, как мы поместили модель Post в пространство имён, мы можем начать создавать CRUD-приложения.

Просмотр всего

Для начала, давайте обновим действие index, чтобы мы могли видеть всё. Я люблю видеть все сообщения прямо на главной странице, поэтому начнём с этого. Откройте файл controllers/SiteController.php и обновите действие index как показано ниже:

public function actionIndex()
{
    $post = new Post;
    $data = $post->find()->all();
    echo $this->render('index', array(
        'data' => $data
    ));
}

Отмечу несколько вещей. Во-первых, ::model()-> исчезло. Данные из ActiveRecord и модели теперь доступны по прямому вызову метода. Например, $post->find()->all(). Несмотря на то, что лично я люблю Post::model()->findAll(), новый способ доступа к данным выглядит более стандартным и легким для чтения.

Во-вторых, findAll был заменён на find()->all(). Все методы поиска теперь исходят от find() или findBySql().

В-третьих, $this->render() теперь требует echo в начале строки. Лично я ненавижу это. Это очень похоже на CakePHP и, на мой взгляд, избыточно. Идея, стоящая за этим изменением, в том, что всё, что вы хотите показать пользователю должно быть отправлено через echo, иначе должно быть просто помещено в переменную для дальнейших действий. Я предпочитаю старый способ рендеринга в переменную (передачу параметра в метод рендеринга), но, возможно, я сживусь в этим.

Теперь обновим страницу…

Если вы знакомы с пространствами имён, вы, вероятно, закричите на меня, спрашивая почему я не включил модель Post. Если же не знакомы, то, возможно, будете удивлены тем, что получили ошибку. Причина проста. _Вы должны помнить пространства имён в Yii2_. Всё что вы хотите использовать должно быть чётко указано, если это не было сделано ранее.

Добавьте следующую строку в начале файла SiteController. Затем обновите страницу.

use app\models\Post;

Теперь добавим разметку для отображения постов. Откройте файл protected/views/site/index.php и замените его содержимое на следующее:

<?php use yii\helpers\Html; ?>

<?php echo Html::a('Create New Post', array('site/create'), array('class' => 'btn btn-primary pull-right')); ?>
<div class="clearfix"></div>
<hr />
<table class="table table-striped table-hover">
    <tr>
        <td>#</td>
        <td>Title</td>
        <td>Created</td>
        <td>Updated</td>
        <td>Options</td>
    </tr>
    <?php foreach ($data as $post): ?>
        <tr>
            <td>
                <?php echo Html::a($post->id, array('site/read', 'id'=>$post->id)); ?>
            </td>
            <td><?php echo Html::a($post->title, array('site/read', 'id'=>$post->id)); ?></td>
            <td><?php echo $post->created; ?></td>
            <td><?php echo $post->updated; ?></td>
            <td>
                <?php echo Html::a(NULL, array('site/update', 'id'=>$post->id), array('class'=>'icon icon-edit')); ?>
                <?php echo Html::a(NULL, array('site/delete', 'id'=>$post->id), array('class'=>'icon icon-trash')); ?>
            </td>
        </tr>
    <?php endforeach; ?>
</table>

Хммм, выглядит иначе, не так ли? CHtml::link() пропало, вместо него появился хелпер Html. К счастью, структура CHtml::link и Html::a не отличается. Так что просто заполняйте параметры.

Читаем

Чтение это просто, поэтому давайте разберёмся с ним. Создайте новый метод в SiteController со следующим определением:

public function actionRead($id=NULL)
{
    echo 'HelloWorld';
}

Теперь перейдите к ?r=site/read&id=1. Вы увидите HelloWorld на экране. Видите? Хорого. Это значит, что наш метод был вызван. Теперь обновим его, чтобы показывать данные из базы.

Во-первых, давайте добавим HttpException в SiteController чтобы мы могли бросать HttpException если пост не найден.

use \yii\base\HttpException;

Теперь дополним действие read

public function actionRead($id=NULL)
    {
        if ($id === NULL)
            throw new HttpException(404, 'Not Found');

        $post = Post::find($id);

        if ($post === NULL)
            throw new HttpException(404, 'Document Does Not Exist');

        echo $this->render('read', array(
            'post' => $post
        ));
    }

Для ясности, HttpException, по существу, это CHttpException. Всё, что мы делаем, это запрашиваем пост с указанным ID в базе данных и показываем его. Если пост не найден либо ID не был указан, мы бросаем HttpException.

Далее, мы должны создать новый файл protected/views/site/read.php и добавить в него следующий код для отображения поста.

<?php use yii\helpers\Html; ?>
<div class="pull-right btn-group">
    <?php echo Html::a('Update', array('site/update', 'id' => $post->id), array('class' => 'btn btn-primary')); ?>
    <?php echo Html::a('Delete', array('site/delete', 'id' => $post->id), array('class' => 'btn btn-danger')); ?>
</div>

<h1><?php echo $post->title; ?></h1>
<p><?php echo $post->content; ?></p>
<hr />
<time>Created On: <?php echo $post->created; ?></time><br />
<time>Updated On: <?php echo $post->updated; ?></time>

Теперь, на главной странице нажмите на «Example Post». Вуаля! Теперь вы можете просматривать посты в блоге!

Удаление

Удаление постов так же просто, поэтому им и займёмся. Создайте новый метод со следующим кодом:

public function actionDelete($id=NULL)
{

}

Для этого метода нам нужен слегка более сложный синтаксис. Мы должны перенаправить пользователя обратно на главную страницу после успешного удаления поста. Начнём.

Во-первых, опишем метод

public function actionDelete($id=NULL)
{
    if ($id === NULL)
    {
        Yii::$app->session->setFlash('PostDeletedError');
        Yii::$app->getResponse()->redirect(array('site/index'));
    }

    $post = Post::find($id);

    if ($post === NULL)
    {
        Yii::$app->session->setFlash('PostDeletedError');
        Yii::$app->getResponse()->redirect(array('site/index'));
    }

    $post->delete();

    Yii::$app->session->setFlash('PostDeleted');
    Yii::$app->getResponse()->redirect(array('site/index'));
}

Несколько замечаний касательно Yii2. Первое, перенаправление теперь делается с помощью Yii::$app->getResponse->redirect() вместо $this->redirect(). Данное решение имеет смысл в плане организации кода, но это так долго печатать! В добавок, это создаёт ощущение перегруженности $app. В тоже время, определение метода осталось прежним.

Второе, setFlash теперь доступно через $app вместо app(). Вы должны теперь к этому привыкнуть. =)

Теперь мы закончили с удалением. Давайте вернёмся к protected/views/site/index.php и поймаем уведомления, которые послали.

Просто добавьте это после первого тэга hr

<?php if(Yii::$app->session->hasFlash('PostDeletedError')): ?>
<div class="alert alert-error">
    There was an error deleting your post!
</div>
<?php endif; ?>

<?php if(Yii::$app->session->hasFlash('PostDeleted')): ?>
<div class="alert alert-success">
    Your post has successfully been deleted!
</div>
<?php endif; ?>

Теперь попробуйте удалить «Example Post». Довольно просто, да? Теперь вы поняли идею Yii::$app, правда?

Создаём

Теперь перейдём к весёлым вещаем, созданию новых записей в нашем блоге. Мы должны сделать несколько вещей для создания. Во-первых, мы собираемся использовать ActiveForm для работы с формой. Во-вторых, мы должны ловить и валидировать данные в $_POST. И, наконец, после этого мы должны сохранить данные в базу. Начнём.

Во-первых, сделаем view для формы. Создайте файл protected/views/site/create.php. Так как мы собираемся использовать виджет, то необходимо создать папку «assets» в корне приложения и сделать её доступной для записи вебсервером. Chmod 755 обычно решает этот вопрос. Затем добавьте определение метода в SiteController.

public function actionCreate()
{
    $model = new Post;
    if (isset($_POST['Post']))
    {
        $model->title = $_POST['Post']['title'];
        $model->content = $_POST['Post']['content'];

        if ($model->save())
            Yii::$app->response->redirect(array('site/read', 'id' => $model->id));
    }

    echo $this->render('create', array(
        'model' => $model
    ));
}

Выглядит более-менее также, как и в Yii1. Но всё же есть несколько различий. Во-первых, у Controller теперь есть метод «populate» ($this->populate($ds, $model)), который в теории должен избавить нас от всего этого убожества с isset($_POST). Код создания нового поста будет выглядеть так:

if ($this->populate($_POST, $model))
{
    //Then do something
}

К сожалению, я не смог заставить его работать в последней версии. Данные в моей модели оставались неизменными. Во-вторых, я так же не смог заставить работать $model->attributes = $_POST[‘Post’]. ActiveRecord выглядит несколько сыро, поэтому пока что приходится вносить данные руками.

Наконец, я наткнулся на ещё одну преграду — сохранение в базу с уникальным ID. Так что и это мы должны делать вручную. Если кто-то выяснит как это решить — не стесняйтесь оставить комментарий.

Давайте обновим модель Post, чтобы заставить уникальный первичный ключ работать. Просто добавьте этот код в конец файла:

public function beforeSave($insert)
{
    if ($this->isNewRecord)
    {
        $command = static::getDb()->createCommand("select max(id) as id from posts")->queryAll();
        $this->id = $command[0]['id'] + 1;
    }

    return parent::beforeSave($insert);
}

Всё что он делает, это проверяет создаётся ли новая запись и если да, то получает максимальный ID из базы, инкрементирует его и использует в качестве ID.

Я пробовал несколько разных комбинаций (NULL, 0, _ for $model->id, но по каким-то причинам ActiveRecord отказывалась сохранять модель с ID отличным 0. У меня нет никаких мыслей почему это не работает).

(На самом деле, автор просто забыл указать AUTO_INCREMENT у поля id. Но я решил оставить эту часть как небольшой урок невнимательности. Прим. переводчика.)

С этим разобрались, теперь создаём view.

<?php use yii\helpers\Html; ?>

<?php $form = $this->beginWidget('yii\widgets\ActiveForm', array(
    'options' => array('class' => 'form-horizontal'),
)); ?>
    <?php echo $form->field($model, 'title')->textInput(array('class' => 'span8')); ?>
    <?php echo $form->field($model, 'content')->textArea(array('class' => 'span8')); ?>
    <div class="form-actions">
        <?php echo Html::submitButton('Submit', null, null, array('class' => 'btn btn-primary')); ?>
    </div>
<?php $this->endWidget(); ?>

Вот и всё, теперь вы можете сохранять модель. Но кое-что странно, не так ли? Например, почему время создания и обновления — 0? Что если мы отправим пустую форму?

Давайте исправим эти две ошибки, прежде чем продолжим. Откроем модель Post и добавим следующий метод:

public function rules()
{
    return array(
        array('title, content', 'required'),
    );
}

Этот метод делает поля title и content обязательным. Теперь при попытке сохранения модели, вы получите ошибку, если одно из этих полей пустое. И так как мы используем bootstrap, то очень легко увидеть в чём именно была ошибка. Попробуйте!

Далее мы должны автоматически выставлять верное время.

Во-первых, добавим ещё одну use-строку вверху нашей модели.

use \yii\db\Expression;

Во-вторых, обновим метод beforeSave для автоматизации процесса.

Внутри блока if ($this->isNewRecord) добавьте строку:

$this->created = new Expression('NOW()');

Перед return parent::beforeSave($insert) добавьте:

$this->updated = new Expression('NOW()');

В итоге метод должен иметь такой вид:

public function beforeSave($insert)
{

    if ($this->isNewRecord)
    {
        $this->created = new Expression('NOW()');
        $command = static::getDb()->createCommand("select max(id) as id from posts")->queryAll();
        $this->id = $command[0]['id'] + 1;
    }

    $this->updated = new Expression('NOW()');
    return parent::beforeSave($insert);
}

Попробуем сохранить ещё раз. Теперь модель валидирует поля title и content, а также автоматически заполняет время создания и обновления. Перейдём к обновлению.

Обновляем

Действие обновления будет практически идентично созданию. Единственное отличие в том, как мы определяем модель, которую будем использовать.

В действии создания мы делали так:

$model = new Post;

В действии обновления сделаем так:

$model = Post::find($id);

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

public function actionUpdate($id=NULL)
{
    if ($id === NULL)
        throw new HttpException(404, 'Not Found');

    $model = Post::find($id);

    if ($model === NULL)
        throw new HttpException(404, 'Document Does Not Exist');

    if (isset($_POST['Post']))
    {
        $model->title = $_POST['Post']['title'];
        $model->content = $_POST['Post']['content'];

        if ($model->save())
            Yii::$app->response->redirect(array('site/read', 'id' => $model->id));
    }

    echo $this->render('create', array(
        'model' => $model
    ));
}

Заметили кое-что интересное? Мы по-прежнему используем view для создания, потому что они абсолютно одинаковы. Круто, да?

Выводы

Итак, у нас получилось. За несколько часом мы прошли путь от полного незнания Yii2 к простому CRUD-приложению на нём. Используя эти знания, вы легко можете расширить приложения, добавив в него поддержку пользователей, аутентификацию, дополнительные таблицы в базе данных и даже куда более мощные фичи.

Yii2 очень похож Yii 1.x, однако в нём много изменений, которые вы должны научиться использовать. Так как Yii2 пока не очень хорошо задокументирован, я написал эту статью лишь на основе исходного кода на Github. Код фреймворка очень хорошо документирован. И так как методы очень похожи на Yii1, то было несложно найти то, что мне было нужно.

Как мы обнаружили, есть ещё несколько проблем, которые должны быть исправлены (либо через лучшее документирование ActiveRecord, либо через исправление того, что сломано).

Ссылки

Исходный код на Github
Демо
Исходная статья