Учебник yii2
1 урок
Для того, чтобы выполнять упражнения из учебника понадобятся инструменты composer и git. Не отчаивайтесь, если вам не известны эти инструменты, нужно будет лишь выполнить несколько команд, которые будут описаны.
Разработчики данного интерактивного курса:
Создание сайта с использованием Yii 2.x
В данном учебнике описывается процесс создания сайта. Каждый шаг разработки описан максимально подробно и может быть применён при создании других приложений. В дополнение к полному руководству и API, данное пособие показывает, вместо полного и подробного описания, пример практического применения фреймворка Yii.Для того, чтобы выполнять упражнения из учебника понадобятся инструменты composer и git. Не отчаивайтесь, если вам не известны эти инструменты, нужно будет лишь выполнить несколько команд, которые будут описаны.
Разработчики данного интерактивного курса:
- Евгений Ткаченко (et.coder@gmail.com)
Начальная установка
Установим стартовый шаблон приложения [Yii 2 Advanced Project Template]. Для этого необходимо выполнить команды, из корневой директории учебника(yii2-tutorial):composer global require "fxp/composer-asset-plugin:1.0.0"
composer create-project --prefer-dist yiisoft/yii2-app-advanced yii2-app-advanced
Если возникают сложности, то ознакомьтесь с
официальным руководством
Процесс установки шаблона первый раз длительный, поэтому давайте пока познакомимся со структурой учебника.
Для этого откройте директорию yii2-tutorial
и осмотритесь:/scripts/ -> Скрипты для работы учебника
/yii2-app-advanced/ -> Сюда будет загружено демонстрационное приложение
/.gitignore -> Файл конфигурация, для Git
/readme.md -> Начальное описание учебника
После того как процесс установки демонстрационного приложения будет окончен, то можно перейти в директорию
yii2-app-advanced
. Для его инициализации, запуска необходимо выполнить команду в этой директории:php init --env=Development
После этого будут сгенерированы некоторые файлы для работы всего приложения и будет установлен режим отладки.
Теперь вы можете перейти по ссылке, чтобы убедится в
работоспособности вашего сайта.Дополнительная информация для самостоятельного ознакомления:
- Ознакомьтесь с информацией об Yii
официальном
руководстве.
- Ознакомьтесь с информацией о запуске приложения
официальном
руководстве.
2 урок
Знакомство с шаблоном приложения Advanced
Для перехода к следующему упражнению, выполните команду из директории yii2-tutorial
В последствии будет установлен "Шаблон приложения advanced", который станет доступен по ссылке.git checkout -f step-0
Пожалуйста, ознакомьтесь с официальным руководством, для того чтобы иметь представление, как устроен "Шаблон приложения advanced".Все статичные страницы нашего приложения не требуют каких-либо данных. А вот страницаSignup
(регистрация пользователей) требует подключения к базе данных. Если на этой странице ввести в поля какие-либо данные и нажать кнопку "Signup", то скорее всего увидите ошибкуDatabase Exception...
.
Сейчас наш сайт пытается подключится к базе данныхyii2advanced
MySQL. Yii не ограничивает вас в выборе базы данных, вы можете легко изменить базу данных, будь то MySQL, MSSQL, PostgreSQL или другие. Для обучения будем использовать SQLite, так как она компактная и не требует накладных расходов. Знать тонкости синтаксиса SQLite не придётся, так как в большинстве случаев вместо SQL будет использоваться ORM подход.
Обратите внимание, что для работы PHP и SQLite потребуется подключение php_pdo_sqlite. Проверьте подключено ли оно у вас.Поменяем настройки подключения к базе данных для всего сайта:
Зайдите в/yii2-app-advanced/common/config/
. В этой директории хранятся файлы конфигурации для работы всех (клиентской, административной, и других) частей сайта. В файлеmain-local.php
:
Компонент<?php return [ 'components' => [ 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=yii2advanced', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ], 'mailer' => [ 'class' => 'yii\swiftmailer\Mailer', 'viewPath' => '@common/mail', // send all mails to a file by default. You have to set // 'useFileTransport' to false and configure a transport // for the mailer to send real emails. 'useFileTransport' => true, ], ], ];
mailer
(компонент отправки почты) оставим без изменений. А вот настройки компонентаdb
изменим.
Подробнее о компонентах в официальном руководстве
В нашем и предыдущем случае за соединение с базой данной отвечает класс<?php return [ 'components' => [ 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'sqlite:' . __DIR__ .'/../../sqlite.db', ], ], ];
yii\db\Connection
.
Рекомендуется ознакомится с API класса ConnectionДля соединения нужно указать DSN, в нашем случае это путь к файлу -/yii2-app-advanced/sqlite.db
. Для остальных, наподобие:
'dsn' => 'pgsql:host=localhost;port=5432;dbname=mydatabase', // PostgreSQL 'dsn' => 'cubrid:dbname=demodb;host=localhost;port=33000', // CUBRID 'dsn' => 'sqlsrv:Server=localhost;Database=mydatabase', // MS SQL Server, sqlsrv 'dsn' => 'dblib:host=localhost;dbname=mydatabase', // MS SQL Server, dblib driver 'dsn' => 'mssql:host=localhost;dbname=mydatabase', // MS SQL Server, mssql driver 'dsn' => 'oci:dbname=//localhost:1521/mydatabase', // Oracle
Имя источника данных (DSN) - это логическое имя, которое используется ODBC (Open Database Connectivity), чтобы обращаться к диску и другой информации, необходимой для доступа к данным.
После настройки подключения, необходимо наполнить данные в базу данных. Для это будем использовать "миграции". Для чего нужны миграции? Вот сейчас нужно заполнить sqlite данными, создать таблицы и чтобы не описывать sql запросы, которые вы должны выполнить, была создана одна миграция. Всё что вам нужно сделать, это выполнить консольную команду в
yii2-app-advanced
:
После этого увидите, что-то вроде:php yii migrate
Теперь вyii2-tutorial\yii2-app-advanced>php yii migrate Yii Migration Tool (based on Yii v2.0.3) Total 1 new migration to be applied: m130524_201442_init Apply the above migration? (yes|no) [no]:y *** applying m130524_201442_init > create table {{%user}} ... done (time: 0.059s) *** applied m130524_201442_init (time: 0.111s) Migrated up successfully.
yii2-app-advanced
можно обнаружить файлsqlite.db
- это и есть наша база данных.
Ну что ж, вернёмся на Signup и попробуем ввести регистрационные данные:Username
-admin
,Email
-admin@local.net
,Password
-123456
. После отправки данных, произойдёт переход на главную страницу с последующей аутентификацией пользователяadmin
. Сейчас мы находимся в пользовательском приложении (frontend). ШаблонAdvanced
также реализует административное приложение(backend). Чтобы попасть в него, просто перейдите по ссылке. На данный момент backend скуден по функционалу, чем frontend. Далее постараемся исправить эту ситуацию.
3 урок
Виды и шаблоны
В этом разделе рассмотрим как создать новую страницу со статическим текстом.
Чтобы начать, выполните команду из директории yii2-tutorial
Перейдите по ссылке вы попадёте на статическую страницу "About".git checkout -f step-0.1
Если посмотреть на адрес ссылки, то можно увидетьindex.php?r=site%2Fabout
.index.php
это входной скрипт нашего приложения. Именно через него идут все запросы пользователя на исполнение. Дальше связкаsite%2Fabout (эквивалентно site/about)
.site
- имя контроллера, который обрабатывает наш запрос,about
- действие, в контроллере которое мы вызываем. Т.е., внутри, Yii переделываетsite
в классSiteController
, аabout
в методfunction actionAbout() {...}
и вызывает его на исполнение.
Найдём этот контроллер и этот метод. Контроллер лежит вyii2-app-advanced/frontend/controllers/SiteController.php
, по умолчанию все контроллеры принято располагать в папкеcontrollers/
c суффиксомController
(Почему так?).
Подробнее о контроллерах и действиях в официальном руководствеВ контроллереSiteController
сейчас вызов статической страницы реализован, черезactionAbout()
:
Метод возвращает статический текст, который состоит из шаблона и вида.public function actionAbout() { return $this->render('about'); }
Виды - это часть MVC архитектуры, это код, который отвечает за представление данных конечным пользователям.
Шаблоны - особый тип видов, которые представляют собой общие части разных видов.
Подробнее о видах и шаблонах в официальном руководствеВ данном случае у нас'about'
- это вид, файл который лежит в директорииyii2-app-advanced/frontend/views/site/
.
Из содержимого файла<?php use yii\helpers\Html; /* @var $this yii\web\View */ $this->title = 'About'; $this->params['breadcrumbs'][] = $this->title; ?> <div class="site-about"> <h1><?= Html::encode($this->title) ?></h1> <p>Это статическая страница, которая может быть изменена в файле:</p> <code><?= __FILE__ ?></code> </div>
about.php
можно понять, что доступен объект$this
- yii\web\View. Этот объект достен во всех видах и шаблонах. В данном случае у нас используется его свойство,$this->title
, которое отвечает за заголовок открытой страницы. Также этот заголовок передаётся в "Навигационную цепочку", через
Попробуйте поменять заголовок "About" на текст "О нас!" и откройте страницу.$this->params['breadcrumbs'][] = $this->title;
Видно, что в меню, всё ещё осталось "About". Чтобы это исправить, нужно внести правки в код этого меню. В роли меню выступает виджет yii\bootstrap\Nav.
Виджеты представляют собой многоразовые строительные блоки, используемые в видах для создания элементов пользовательского интерфейса.
Подробнее о виджетах в официальном руководствеВиджет меню подключается в шаблоне, который подключается перед показом видаabout
. Чтобы определить какой шаблон используется, то нужно обратиться к текущему контроллеру`SiteController
и его методуrender()
, которое мы вызываем в нашем действииactionAbout()
. Методrender()
обращается к свойствуlayout
текущего контроллера, для определения шаблона. Если это свойство не задано у контроллера, то ищется шаблон экземпляра приложения. В данном случае в роли экземпляра приложения выступает классyii\web\Application
, который создаётся при запуске приложения.
Подробнее о процессе "Запуск приложения" в официальном руководствеВ общем, обычно всё находится в директорииyii2-app-advanced/frontend/views/layouts
в файлеmain.php
. Иногда разработчики приложения изменяют эту ситуацию, настраивая конфигурациюyii\web\Application
илиSiteController
на своё усмотрение. Yii никого в этом не ограничивает. В данный момент, ограничимся тем, что имеем по умолчанию.
Итак, откройтеyii2-app-advanced/frontend/views/layout/main.php
и ознакомьтесь с содержимым. Это шаблон, который подключается к каждому виду. Т.е. по сути это главная HTML разметка для всех страниц приложения. Виджет меню имеет вид
Найдите код внутри этого виджетаuse yii\bootstrap\Nav; NavBar::begin([ //... NavBar::end();
И просто изменить['label' => 'About', 'url' => ['/site/about']],
label
на
Можете самостоятельно попробовать изменить все остальные пункты меню.['label' => 'О нас', 'url' => ['/site/about']],
Как видно, всё ещё остались "My Company" и "Home". Возможно вы уже поменяли "My Company" на свой текст, просто заменив его. У приложения есть свойства, которые доступны для конфигурации. А именно "имя приложения". Оно как раз подходит для того, чтобы быть задействованным в данном случае. Настроим его.
Для этого нужно обратиться к настройкам приложения. Мы это уже делали в уроке, когда знакомились с шаблоном приложения Advanced. Только на этот раз, это будет файл неyii2-app-advanced/common/config/main-local.php
, аmain.php
в той же директории. И измените его на
Изменив<?php return [ 'name' => 'Мой сайт', 'language' => 'ru', 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', 'components' => [ 'cache' => [ 'class' => 'yii\caching\FileCache', ], ], ];
'language' => 'ru',
, изменился основной язык приложения. Доступные языки для приложения можно обнаружить вyii2-app-advanced/vendor/yiisoft/yii2/messages/
. Все возможные сообщения, которые были на английском, станут на русском.
Свойство'name' => 'Мой сайт',
доступно в качестве конструкцииYii::$app->name
. Не сложно догадаться, что`Yii::$app->language
вернётru
. Т.е. к любому свойству приложения можно обратиться именно так.
Изменим в главном шаблоне "My Company" наYii::$app->name
и чуть ниже в footer:NavBar::begin([ 'brandLabel' => Yii::$app->name,
<p class="pull-left">© <?= Yii::$app->name ?> <?= date('Y') ?></p>
Хочу ещё больше статических страниц!
Когда у вас будет много статический страниц "О нас", "Режим работы", "Доставка" и прочее, то не совсем удобно, каждый раз в контроллере создавать метод:
Для этого уже реализовано одно действие для контроллеров. Этоclass SiteController extends Controller { public function actionAbout() { return $this->render('about'); } public function actionDuty() { return $this->render('duty'); } public function actionDelivery() { return $this->render('delivery'); } }
yii\web\ViewAction
Рекомендуется ознокомится с API класса ViewAction и перечитать про отдельные действия в контроллерах.Найдите вSiteController
методactions
, в котором уже имеется:
По умолчанию в Advanced этого кода нет, он добавлен для вашего удобства. Теперь нужно перейти по адресу index.php?r=site/page&view=about'page' => [ 'class' => 'yii\web\ViewAction', ],
Попробуйте поменять в адресной строке параметрview
на duty или delivery. Проанализируйте результаты.
4 урок
Работа с формами
В этом разделе рассмотрим как создать форму.
Чтобы начать, выполните команду из директории yii2-tutorial
Как выглядит форма, созданная с помощью Yii2, можно увидеть по ссылке.git checkout -f step-0.2
Иногда можно увидеть на этой странице ошибку
"Invalid Configuration – yii\base\InvalidConfigException Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required.".
Связана она с тем, что на этой странице используется CAPTCHA и ей необходима GD или ImageMagick PHP библиотеки.
Если всё в порядке, то продолжим. По адресу ссылкиindex.php?r=site%2Fcontact
, где находится форма, можно увидеть, что используется всё тот жеSiteController
контроллер, что и в предыдущем уроке. Только тут действие другое -contact
. Следовательно открываем\frontend\controllers\SiteController::actionContact
:
Видим знакомый ужеpublic function actionContact() { $model = new ContactForm(); if ($model->load(Yii::$app->request->post()) && $model->validate()) { if ($model->sendEmail(Yii::$app->params['adminEmail'])) { Yii::$app->session->setFlash('success', 'Спасибо за ваше письмо. Мы свяжемся с вами в ближайшее время.'); } else { Yii::$app->session->setFlash('error', 'Ошибка отправки почты.'); } return $this->refresh(); } else { return $this->render('contact', [ 'model' => $model, ]); } }
$this->render
, где первым параметром передаётся название вида - в данном случае используется вид'contact'
. Открываем егоyii2-app-advanced/frontend/views/site/contact.php
.
Чтобы создать в Yii html код формы:
нужно обратиться за помощью к виджету<form action="..." method="post"> <input ...> <input ...> <input ...> <button> </form>
\yii\widgets\ActiveForm
. Наследник этого виджета -yii\bootstrap\ActiveForm;
, и используется вcontact.php
. Отличия\yii\widgets\ActiveForm
отyii\bootstrap\ActiveForm
в том, что последний выводит элементы формы с учётом требований Bootstrap.
На первых порах возникает вопрос - зачем использовать этот виджет и вообще php код, если можно использовать HTML. Во-первых использование виджета ускоряет процесс создания рутинных, обычных форм и делает легким процесс проверки пользовательских данных.
Вcontact.php
Методы<?php $form = ActiveForm::begin(['id' => 'contact-form', 'enableClientValidation' => false]); ?> <?= $form->field($model, 'name') ?> <?= $form->field($model, 'email') ?> <?= $form->field($model, 'subject') ?> <?= $form->field($model, 'body')->textArea(['rows' => 6]) ?> <?= $form->field($model, 'verifyCode')->widget( Captcha::className(), [ 'template' => '<div class="row"><div class="col-lg-3">{image}</div><div class="col-lg-6">{input}</div></div>', ] ) ?> <div class="form-group"> <?= Html::submitButton('Отправить', ['class' => 'btn btn-primary', 'name' => 'contact-button']) ?> </div> <?php ActiveForm::end(); ?>
ActiveForm::begin
иActiveForm::end();
выводят открывающий и закрывающий теги формы. Между этими методами с помощью метода ActiveForm::field создаются элементы формы.
Рекомендуется ознакомится с API класса ActiveFormВы наверное обратили внимание, что в каждый элемент формы передаётся переменная $model. Yii использует MVC («модель-представление-контроллер») шаблон проектирования приложения. Вы уже знакомы с контроллерами и представлениям, так вот переменная$model
- это недостающее звено, класс который описывает данные и предоставляет методы для работы с этими данными. В данном случае, модель описывает "деловое предложение" или "вопрос" от пользователя, т.е. "обратную связь". В Yii для работы с моделью реализован базовый класс yii\base\Model. Этот класс предоставляет методы, которые:
- помогают наполнять модель данными,
- читать данные из модели,
- проверять данные на корректность;
Откройте классpublic function actionContact() { $model = new ContactForm(); return $this->render('contact', ['model' => $model,]); }
ContactForm
, он находится вyii2-app-advanced/frontend/models/
.
Принято все модели располагать в директории `application/models/`, но вы всегда можете расположить их где угодно с учётом стандарта PSR-4.
Сначала перечисляются все атрибуты модели, это - имя пользователя($name), электронный адрес($email), тема сообщения($subject), само сообщение($body) и CAPTCHA(verifyCode). Эти атрибуты описывают модель - сущность "обратная связь". Дальше идёт методclass ContactForm extends Model { public $name; public $email; public $subject; public $body; public $verifyCode; public function rules() { } public function attributeLabels() { } public function sendEmail($email) { }
rules()
, который используется для валидации, проверки атрибутов модели. Второй методattributeLabels
используется для описания меток, маркировок атрибутов на понятном для человека языке. Обычно эти метки используются в представлении для описания элементов форм или другого. Следующий методsendEmail
отвечает за отправку "обратной связи" на электронный адрес администратора сайта.
Как всё работает? (упрощённо)
Запрос от пользователя поступает на входной скриптweb/index.php
. В скрипте создаётся приложениеyii\web\Application
с учётом конфигураций. Приложение определяет маршрут - контроллер и действие. Создаётся экземпляр контроллера и вызывается действие. В действии создаёт модель и контроллер передаёт её в вид. Далее генерируется конечный ответ, с учётом шаблонов, видов и данных из моделей. Ответ отдаётся пользователю. Пользователь вводит данные и отправляет их опять в входной скрипт. Всё повторяется до контроллера. Теперь в контроллере опять создаётся модель, но перед отправкой её в представление, она наполняется данными с помощью методаyii\base\Model->load()
и затем данные проверяются методомyii\base\Model->validate()
:
Модель наполняется пользовательскими данными с помощью компонента requestpublic function actionContact() { $model = new ContactForm(); if ($model->load(Yii::$app->request->post()) && $model->validate())
Yii::$app->request->post()
. В первом уроке мы уже знакомились с одним из компонентномdb
, который служил для настройки базы данных. Любой компонент приложения может быть вызван какYii::$app->имя_компонента
. ГдеYii::$app
- это приложение, которое было создано в входном файлеindex.php
, а имя компонента можно установить через конфигурацию приложения. Компонентrequest
(служит для работы с HTTP запросами yii\web\Request), с помощью методаpost
возвращает$_POST
данные, которые поступают в метод$model->load
. Этот метод модели соотносит её атрибуты с данными из формы, по принципу
В данном случае это:имя_модели[атрибут] = данные[имя_элемента_формы][атрибут]
Данные из формы могут быть какими угодно, поэтому их следует проверить, перед тем как с ними работать. Одну часть работы по проверке данных делает всё тот же методContactForm['name'] = $_POST['ContactForm']['name'] ContactForm['email'] = $_POST['ContactForm']['email'] // и так далее.
load
. Он определяет каким атрибутам можно задавать значения. Так если бы пользователь отправил из формы данные с именем $_POST['ContactForm']['hack'], то они бы не попали в модель, так как у модели нет атрибута$hack
. Также вload
срабатывает внутренний механизм, который смотрит метод моделиrules
и извлекает из него все атрибуты которые там упоминаются. Если какого-то атрибута вrules
нет, то атрибут модели считается не безопасным.
Видно, что все возможные атрибуты модели встречаются в коде rules, поэтому в данном случае они все являются безопасными. Т.е. для них выполняется условие:public function rules() { return [ [['name', 'email', 'subject', 'body'], 'required'], ['email', 'email'], ['verifyCode', 'captcha'], ]; }
Методимя_модели[атрибут] = данные[имя_элемента_формы][атрибут]
$model->validate()
, который запускает вторую часть проверки данных, формирует из результата методаrules()
различные проверки. Делает он это по следующему принципу:
- перебирается каждый элемент массива:
`
php [ [['name', 'email', 'subject', 'body'], 'required'], ['email', 'email'], ['verifyCode', 'captcha'], ];`
- каждый элемент разбирается на составляющие: названия атрибутов, название валидатора.
required
- валидатор, который проверяет отправил ли пользователь необходимые данные. Т.е. отправил ли пользователь name, email, subject, body.
email
- валидатор, который проверяет правильность введённого электронного адреса.
captcha
- валидатор, который проверяет правильность введённого проверочного кода.
Список встроенных валидаторов можно посмотреть в официальном руководстве
Пользовательские данные не корректные.
Если какая-нибудь проверка прошла не успешно, то атрибут модели модель$errors
наполняется сообщениями об ошибках с учётом атрибута, в котором возникла ошибка. В результате в контроллере условие не выполняется:
и в вид отправляется модель с сообщениями об ошибках. В видеif ($model->load(Yii::$app->request->post()) && $model->validate()) {
contract.php
с помощьюActiveForm::field
выводятся элементы формы: для тех у кого ошибки, формируются сообщения об ошибках, остальные выводятся со значениями.
По умолчанию в виджетеActiveForm
включено свойство$enableClientValidation
, которое означает, что проверки выполняются с помощью javascript кода прямо в браузере, а не отправляются на сервер. В нашем примере оно отключено, включите его при создании ActiveForm:
Теперь при отправки данных, проверка будет происходить сначала в браузере, без отправки запросов на сервер:$form = ActiveForm::begin(['enableClientValidation' => true]);
Пользовательские данные корректные.
Если же данные прошли проверку в контроллере:
то срабатывает методif ($model->load(Yii::$app->request->post()) && $model->validate()) {
sendEmail
модели, который отправляет сообщение на электронный адрес администратора. Далее с помощью компонента yii\web\Session (отвечает за сессию $_SESSION пользователя) формируется статус-ответ об отправки почты. Дальше с помощьюController->refresh()
отправляется ответ пользователю, который содержит заголовки для обновления текущей страницы.
if ($model->sendEmail(Yii::$app->params['adminEmail'])) { Yii::$app->session->setFlash( 'success', 'Спасибо за ваше письмо. Мы свяжемся с вами в ближайшее время.' ); } else { Yii::$app->session->setFlash('error', 'Ошибка отправки почты.'); } return $this->refresh();
Создание формы.
Построим новую форму - опрос пользователя. Форма будет содержать следующие элементы:
- Ф.И.О.
- Пол
- Вопрос - какие планеты солнечной системы обитаемы?
- Вопрос - какие космонавты известны?
- Вопрос - на какую планету хотели бы полететь?
- Проверочный код - каптча
yii\base\Model
позволяет:
- наполнять модель данными,
- извлекать данные из модели,
- проверять данные на корректность;
yii\base\Model
, расширяя его до такого состояния, при котором модель становится отражением строки в таблице из базы данных. Следовательно с моделью можно работать так же как со строкой в базе данных - искать, создавать, изменять, удалять. Следующий код иллюстрирует всю мощь и красоту реализованного шаблона проектирования Active Record в Yii:
С помощью трёх строк можно наполнить модель данными, проверить данные и в случае корректности, сохранить их в базу данных. Заметьте SQL не использовался, этот код сработает и для используемой нами SQLite и для любой другой СУБД.$model = new ActiveRecord; $model->attributes = ['text' => 'Длинный текст', 'title' => 'Заголовок']; $model->save();
И так вернёмся к форме "Опрос". Создадим для начала таблицу в базе данных.
Напомним, что для обращения к базе данных используется компонент, который мы настроили вyii2-app-advanced/common/config/main-local.php
конфигурации приложения:
и к нему можно обратиться через'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'sqlite:' . __DIR__ .'/../../sqlite.db', ],
\Yii::$app->db
. Познакомится с методами и свойствами компонентаdb
можно в API класса yii\db\Connection.
Для создания в базе данных таблицы, которая будет хранить данные из опросов, нам понадобится миграция. Сейчас она создана, вам её осталось только применить.
Миграции создаются следующим образом:
Вyii2-tutorial\yii2-app-advanced> php yii migrate/create interview Yii Migration Tool (based on Yii v2.0.3) Create new migration '~/yii2-tutorial/yii2-app-advanced/console/migrations/m150428_104828_interview.php'? (yes|no) [no]:yes New migration created successfully.
yii2-app-advanced/console/migrations
появится файл, наподобиеm150428_104828_interview.php
, который содержит класс с тем же именем, что и имя файла. Этот класс содержит два методаup()
иdown()
. Первый описывает, что происходит, когда миграция применяется, второй - что происходит, когда миграция аннулируется. Код принято писать так, чтобы он работал для любой СУБД, пусть то MySQL, PostgreSQL, SQlite или другая. Для того, чтобы писать универсальный код для всех СУБД в Yii реализован абстрактный класс yii\db\Schema. Этот класс описывает схему, как хранится информация в СУБД. При создании запроса определяется на основанииdns
компонентаyii\db\Connection
, какую схему нужно использовать. В свою очередь эта схема реализует работу с данными в зависимости от СУБД.
Миграция для таблицы, которая будет храненить данных из формы "Опрос", выглядит следующим образом(Подробнее в в файлеyii2-app-advanced/console/migrations/m150428_104828_interview.php
) :
С применением миграций мы уже сталкивались, когда создавали таблицу$this->createTable('{{%interview}}', [ 'id' => Schema::TYPE_PK, 'name' => Schema::TYPE_STRING . ' NOT NULL', 'sex' => Schema::TYPE_BOOLEAN . ' NOT NULL', 'planets' => Schema::TYPE_STRING . ' NOT NULL', 'astronauts' => Schema::TYPE_STRING. ' NOT NULL', 'planet' => Schema::TYPE_INTEGER . ' NOT NULL', ], $tableOptions);
user
. Применим новую миграцию:
Таблица в базе данных создана. Теперь, чтобы использовать Active Record, необходимо создать модель, как отражение строки из СУБД. Чтобы облегчить эту задачу, в Yii есть замечательный инструмент Gii, который генерирует код.yii2-tutorial\yii2-app-advanced> php yii migrate Yii Migration Tool (based on Yii v2.0.3) Total 1 new migration to be applied: m150428_104828_interview Apply the above migration? (yes|no) [no]:yes *** applying m150428_104828_interview > create table {{%interview}} ... done (time: 0.048s) *** applied m150428_104828_interview (time: 0.116s) Migrated up successfully.
Gii - магический инструмент, который может написать код за вас.
Gii включен в Advanced шаблоне приложения, если это приложение инициализировано в режиме отладки, т.е. как было ранее сделано, через
Чтобы попасть в Gii нужно перейти по ссылке index.php?r=gii и выбрать пункт Model Generator.php init --env=Development
Если ваш сайт установлен не на локальном хосте, то скорее всего вы увидите на странице Gii ошибку доступа 403.
Forbidden (#403) You are not allowed to access this page.
Доступ по умолчанию разрешён только для
['127.0.0.1', '::1'];
IP адресов. Настраивается свойство$allowedIPs
для Gii через конфигурационные файлы в директорииconfig
. Настройки доступа, в зависимости от окружения, на котором запущен сайт, могут изменяться. Поэтому такие настройки принято хранить не вmain.php
, а вmain-local.php
. Откройтеyii2-app-advanced/frontend/config/main-local.php
и измените строку:
на$config['modules']['gii'] = 'yii\gii\Module';
Теперь на страницу index.php?r=gii разрешено будет заходить только с тех устройств, которые находятся в подсети 192.168.0.0/24$config['modules']['gii'] = [ 'class' => 'yii\gii\Module', 'allowedIPs' => ['192.168.0.*'] ];
Вернёмся к Model Generator. Этот раздел Gii предназначен для генерации моделей. Для того, чтобы форма была сгенерирована, необходимо указать:
- имя таблицы
- имя будущей модели
Interview
- пространство имени
frontend\models
models\Interview.php
будущий код. После этого нажмите Generate. Всё наша модель создана и доступна по/yii2-app-advanced/frontend/models/Interview.php
. Gii всё же не всесилен и потребуется внести некоторые изменения в модель. Добавим элемент "проверочный код" -verifyCode
, как свойство модели:
изменим метки для будущих элементов:class Interview extends \yii\db\ActiveRecord { public $verifyCode; //... }
И так модель формы готова, сделаем саму форму. Опять обратимся за помощью к Gii, только теперь выберем генераторpublic function attributeLabels() { return [ 'name' => 'Имя', 'sex' => 'Пол', 'planets' => 'Какие планеты обитаемы?', 'astronauts' => 'Какие космонавты известны?', 'planet' => 'На какую планету хотели бы полететь?', 'verifyCode' => 'Проверочный код', ]; }
Form Generator
, в котором следует указать:
- имя вида (View Name) -
site/interview
- имя модели с учётом пространства имён -
frontend\models\Interview
views/site/interview.php
. Также Gii предложит код действия для контроллера, который необходимо самостоятельно вставить в контроллерSiteController
. Вот чуть измененный код действия:
Итак модель, контроллер с действием и представление созданы, теперь можно посмотреть на результат - index.php?r=site/interviewpublic function actionInterview() { $model = new \frontend\models\Interview(); if ($model->load(Yii::$app->request->post())) { if ($model->validate()) { // делаем что-то, если форма прошла валидацию return; } } return $this->render('interview', [ 'model' => $model, ]); }
Настройка вида формы
Изменим вид формы на
- Вид элемента
name
остаётся неизменным. - Вид элемента
sex
необходимо переделать на два переключателя. По умолчанию$form->field()
генерирует текстовый<input type="text">
. Это можно изменить следующим образом:
ActiveForm->ActiveField->radioList() - метод в качестве первого элемента принимает массив возможных значений. Так как метка атрибута<?= $form->field($model, 'sex')->radioList(['Мужчина', 'Женщина']) ?>
sex
в модели определена как'sex' => 'Пол'
, а необходимо "Вы мужчина/женщина?". То можно изменить "пол" на "вы", что не совсем понятно, если эти метки в других местах (например в письме, отчётных таблицах или в прочем). Поэтому сделаем это только в виде, с помощью ActiveField->label().
<?= $form->field($model, 'sex')->radioList(['Мужчина', 'Женщина'])->label('Вы:') ?>
- Дальше идёт список флаги(checkbox). Для генерации их используем метод ActiveField->checkboxList().
<?= $form->field($model, 'planets')->checkboxList( ['Меркурий', 'Венера', 'Земля', 'Марс', 'Юпитер', 'Сатурн', 'Уран', 'Нептун'] )->label('Какие планеты по вашему мнению обитаемы?') ?>
- Дальше список с множественным выбором(select) и подсказкой(hint) - ActiveField->dropDownList():
ActiveField->hint() - формирует подсказку для элемента формы.<?= $form->field($model, 'astronauts')->dropDownList( [ 'Юрий Гагарин', 'Алексей Леонов', 'Нил Армстронг', 'Валентина Терешкова', 'Эдвин Олдрин', 'Анатолий Соловьев' ], ['size' => 6, 'multiple' => true] ) ->hint('С помощью Ctrl вы можете выбрать более одного космонавта') ->label('Какие космонавты вам известны?') ?>
- Дальше выпадающий список с одиночным выбором - метод
ActiveField->dropDownList()
:
<?= $form->field($model, 'planet')->dropDownList( ['Меркурий', 'Венера', 'Земля', 'Марс', 'Юпитер', 'Сатурн', 'Уран', 'Нептун'] ) ?>
- И виджет каптчи, как элемент формы:
Html шаблона у каптчи формируется исходя из свойства<?= $form->field($model, 'verifyCode')->widget( yii\captcha\Captcha::className(), [ 'template' => '<div class="row"><div class="col-xs-3">{image}</div><div class="col-xs-4">{input}</div></div>', ] )->hint('Нажмите на картинку, чтобы обновить.') ?>
yii\captcha\Captcha::template
. Чтобы настроить само{image}
используемCaptchaAction::minLength
,CaptchaAction::maxLength
,CaptchaAction::height
свойства, которые настраиваются в действииcaptcha
, контроллераSiteController
.
public function actions() { return [ 'captcha' => [ 'class' => 'yii\captcha\CaptchaAction', 'minLength'=>3, 'maxLength'=>4, 'height'=>40, 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, ], //... ]; }
Когда формируется элемент капчта, то для получения изображения виджет yii\captcha\Captcha, по умолчанию, посылает запрос на `site/captcha`. Действие yii\captcha\CaptchaAction возвращает изображение каптчи и при этом сохраняет в сессию пользователя проверочный код, для последующей валидации.Обновите страницу с формой. Вид у неё не такой как у результата, который мы ожидали. Всё дело в том, что в видеsite/interview.php
Gii сгенерировалuse \yii\widgets\ActiveForm
вместоuse yii\bootstrap\ActiveForm;
. Измените пространство имен наyii\bootstrap\
. Как описывалось ранее, это позволит использовать стили Bootstrap для форм. Вид формы настроен.
Валидация формы
МодельInterview
на данный момент использует правила для проверки, которые Gii подобрал на основании типов полей в базе данных. Перепишем в моделиfrontend/models/Interview.php
некоторые правила валидации:
public function rules() { return [ [['name', 'sex', 'planets', 'astronauts', 'planet', 'verifyCode'], 'required'], ['name', 'string'], ['sex', 'boolean', 'message' => 'Пол выбран не верно.'], [ ['planets', 'planet'], 'in', 'range' => range(0, 7), 'message' => 'Выбран не корректный список планет.', 'allowArray' => 1 ], [ 'astronauts', 'in', 'range' => range(0, 5), 'message' => 'Выбран не корректный список космонавтов.', 'allowArray' => 1 ], ['verifyCode', 'captcha'], ]; }
Дополнительная информация для самостоятельного ознакомления:
- Ознакомьтесь с информацией о работе с формами в
официальном
руководстве.
- Ознакомьтесь с информацией о проверке данных
официальном
руководстве.
- Ознакомьтесь с информацией о генерация кода при помощи Gii официальном руководстве.
5 урок
Обработка формы.
В этом разделе рассмотрим как в Yii работать с базой данных, сессиями. Познакомимся с проведениями и событиями.
Чтобы начать, выполните команду из директории yii2-tutorial
В предыдущих главах вы уже узнали: что такое контроллер, что такое модель, что такое виды с шаблонами, как это всё взаимосвязано и где располагается. Поэтому далее многая информация будет дана без разъяснений этих основ.git checkout -f step-0.3
Вспомните, создавая форму "Опрос", контроллерSiteController
ничего в ответ не посылал, когда данные от пользователя поступали валидные:
Логичнее было отправить сообщение пользователю о успешном принятии его ответов. Также необходимо сохранить эти ответы, для последующего анализа. Ещё запретим принимать участие в опросе тем пользователям, кто уже ответил на вопросы.if ($model->load(Yii::$app->request->post())) { if ($model->validate()) { // делаем что-то, если форма прошла валидацию return; } }
Компонент для работы с сессией.
Давайте сначала проинформируем пользователя об успешном принятии его ответов и перенаправим его на домашнюю страницу, после того как он ввёл корректные данные. Для информирования пользователя запишем ему сообщение в сессию. Когда пользователя перебросит на домашнюю страницу, покажем сообщение из сессии и удалим его впоследствии.
Ознакомьтесь с информацией "Управление сессиями в PHP"В Yii2 для работы с сессиями используется компонент yii\web\Session, к которому можно обратиться через\Yii::$app->session
. У компонента Session есть свойство$flash
, которое предназначено именно для нашей задачи. Следующий код
создаст в сессии пользователя сообщение с ключомYii::$app->session->setFlash( 'success', 'Спасибо, что уделили время. В ближайшее время будут опубликованы результаты.' );
success
. Останется только вывести это сообщение. Yii упрощает это до предельно возможной простоты - ничего не нужно делать. В главном шаблонеmain.php
есть код:
это виджет, который располагается в директории<?= \frontend\widgets\Alert::widget() ?>
yii2-app-advanced\frontend\widgets\
. Откройте его и ознакомьтесь.
Alert виджет выводит сообщение из сессии, которое было задано с помощьюsetFlash
и распознаёт по ключу, какой стиль css Bootstrap применить к данном сообщению. В данном случае ключsuccess
подключит css'alert alert-success'
:
Спасибо, что уделили время. В ближайшее время будут опубликованы результаты.И так добавимyii\web\Session->setFlash()
в действие контроллера:
Теперь когда форма запроса будет верно заполнена и отправлена, должно появиться сообщение. Проверьте.public function actionInterview() { $model = new Interview(); if ($model->load(Yii::$app->request->post())) { if ($model->validate()) { Yii::$app->session->setFlash( 'success', 'Спасибо, что уделили время. В ближайшее время будут опубликованы результаты.' ); } } return $this->render('interview', ['model' => $model,]); }
После успешной отправки формы, страница с формой опять выводится на экран, так как сработал$this->render('interview'...)
. Перенаправим пользователя, на домашнюю страницу. Для формирования URL адресов в Yii используется класс помощник yii\helpers\Url. В API этого класса можно найти методhome()
, который перенаправляет на домашнюю страницу. Домашняя страница может быть задана через конфигурацию приложения\Yii::$app->homeUrl
, так же, как настраивали язык и имя нашего приложения:
Url формируется как id контроллера и id действия - это не строка, а массив так как:return [ 'name' => 'Мой сайт', 'language' => 'ru', 'homeUrl' => ['/site/interview'], ]
- вторым и последующим элементом может быть переданы $_GET параметры
'homeUrl' => ['/site/page', 'view'=>'duty'],
- если это строка, то путь будет создан не как
http://localhost:8888/yii2-app-advanced/frontend/web/index.php?r=/site/interview
, а какhttp://localhost:8888/site/interview
.
yii2-app-advanced/common/config/main.php
. Эта конфигурация располагается в директорииcommon
, что означает что конфигурация будет применена ко всем приложениями - консольному, административной части (backend), клиентской части (frontend) и другим. Мы работаем вfrontend
, поэтому homeUrl установим только для него, так как, например, в административной части URL домашней страницы может и не быть.
В файле конфигурацииyii2-app-advanced/frontend/config/main.php
добавьте код:
Теперь при вызове'homeUrl' => ['/site/page', 'view'=>'about'],
Url::homeUrl()
будет сформирован/index.php?r=site/page&view=about
. Чтобы перенаправить пользователя по этому адресу используем метод контроллераredirect()
Теперь когда пользователь ответит на опрос, его перебросит на домашнюю страницу. Но он может схитрить - снова вернуться на страницу с формой и ответить заново. Ограничим доступ к форме, если пользователь уже отвечал. Сделаем это через всю туже сессию - когда пользователь открывать страницу с формой, в контроллере будет срабатывать проверка по поиску определённого ключа в сессии, если он найден, то запретим пользователю дальнейшую работу с формой. Ключ в сессии будем создавать только, когда форма прошла валидацию.use yii\helpers\Url; class SiteController extends Controller { public function actionInterview() { $model = new Interview(); if ($model->load(Yii::$app->request->post())) { if ($model->validate()) { Yii::$app->session->setFlash( 'success', 'Спасибо, что уделили время. В ближайшее время будут опубликованы результаты.' ); return $this->redirect(Url::home()); } } return $this->render('interview', ['model' => $model,]); } }
Такое ограничение нам может понадобиться в будущем в разных ситуациях: участие в акциях, опросах, вручение подарков и прочее. Поэтому давайте сделаем так, чтобы можно было легко использовать наш код в разных действиях контроллеров. Т.е. если писать:
то нужно будет в других действиях проделывать аналогичные действия. В yii\base\Controller есть константы:public function actionInterview() { if (Yii::$app->session->get('уникальный-ключ') === null) { $model = new Interview(); if ($model->load(Yii::$app->request->post())) { if ($model->validate()) { Yii::$app->session->set('уникальный-ключ',1); return $this->redirect(Url::home()); } } return $this->render('interview', ['model' => $model,]); } else { echo "ДОСТУП ЗАКРЫТ"; } }
Из их названия видно, что первое называется "СОБЫТИЕ_ПОСЛЕ_ДЕЙСТВИЯ", второе - "СОБЫТИЕ_ПЕРЕД_ДЕЙСТВИЕМ".const EVENT_BEFORE_ACTION = 'beforeAction'; const EVENT_AFTER_ACTION = 'afterAction';
События и поведения.
Событие — то, что происходит в некоторый момент времени и рассматривается как изменение состояния чего-либо.
Т.е. можно догадаться, что эти две константы описывают методы, которые сработают до действия и после.
Рекомендуется ознакомится с информацией о событиях в Yii 2При срабатывании события EVENT_BEFORE_ACTION, нам необходимо проверить, есть ли ключ в сессии пользователя. А при срабатывании EVENT_AFTER_ACTION нам необходимо установить этот ключ, но с одной оговоркой - если форма корректна.
В контроллере нужно написать, что-то вроде такого:
Сразу возникают вопросы:$this->on( $this::EVENT_BEFORE_ACTION, function () { if (Yii::$app->session->get('уникальный-ключ') !== null) { echo "ДОСТУП ЗАКРЫТ"; } } ); $this->on( $this::EVENT_AFTER_ACTION, function () { Yii::$app->session->set('уникальный-ключ', 1); } );
- Как отключить EVENT_AFTER_ACTION, если данные в форме некорректные и требуют правок со стороны пользователя?
- Куда этот код вставлять?
- Контроллер последовательно вызывает метод beforeAction() приложения и самого контроллера. Если один из методов вернул false, то остальные, невызванные методы beforeAction будут пропущены, а выполнение действия будет отменено; По умолчанию, каждый вызов метода beforeAction() вызовет событие EVENT_BEFORE_ACTION.
- Контроллер запускает действие: параметры действия будут проанализированы и заполнены из данных запроса.
- Контроллер последовательно вызывает методы afterAction контроллера и приложения. По умолчанию, каждый вызов метода afterAction() вызовет событие EVENT_AFTER_ACTION.
Рекомендуется ознакомится с информацией о поведениях в Yii 2У контроллера есть методbehaviors()
, в котором можно описать поведения. Сейчас вSiteController
:
Что этот код обозначает мы разберём в ближайших главах, а сейчас просто добавим своё поведение, назовём его к примеруpublic function behaviors() { return [ 'access' => [ 'class' => AccessControl::className(), 'only' => ['logout', 'signup'], 'rules' => [ [ 'actions' => ['signup'], 'allow' => true, 'roles' => ['?'], ], [ 'actions' => ['logout'], 'allow' => true, 'roles' => ['@'], ], ], ], 'verbs' => [ 'class' => VerbFilter::className(), 'actions' => [ 'logout' => ['post'], ], ], ]; }
accessOnce
.
Нужно указать класс поведения - создадим его. Создайте директориюpublic function behaviors() { return [ 'accessOnce' => [ 'class' => ], //... ]; }
yii2-app-advanced/frontend/behaviors
и в ней класс:
допишем в контроллере:<?php namespace frontend\behaviors; use yii\base\Behavior; class AccessOnce extends Behavior { }
В принципе это "пустышка", т.е. если перейти на страницу с формой "Опроса", то ничего изменится. Но когда поведение прикреплено к наследнику базового класса yii\base\Object, т.е. к почти ко всем классам Yii, то в поведении, после создания его объекта, свойству'accessOnce' => [ 'class' => \frontend\behaviors\AccessOnce::className(), ],
$owner
присваивается объект, который вызвал это поведение. По-простому: объект классаSiteController
является владельцем поведенияAccessOnce
и может быть в поведении получен через$this->owner
. Следовательно, становится доступно влиять на события владельца поведения, через$this->owner->on(...)
. Но опять же, куда вставлять этот код? Логичнее было бы прикрепить обработчик события при создании объекта поведения, делается это через переопределение методаyii\base\Behavior::events()
:
Т.к. поведение может быть прикреплено к разным объектам(контроллерам, моделям, представлениями и прочему), то событий EVENT_BEFORE_ACTION и EVENT_AFTER_ACTION у этих объектов может и не быть. Поэтому вводим дополнительную проверкуclass AccessOnce extends Behavior { public function events() { $owner = $this->owner; if ($owner instanceof Controller) { return [ $owner::EVENT_BEFORE_ACTION => 'имя_обработчика', $owner::EVENT_AFTER_ACTION => 'имя_обработчика', ]; } return parent::events(); } }
которая ограничит неверное использование поведения AccessOnce.if ($owner instanceof Controller) {
Теперь создадим обработчиков, которые будут срабатывать при наступлении событий:
В обработчике будет доступно, $event - наследник класса yii\base\Event Наследник определяется в зависимости от того, кто это событие вызвал. В данном случае $event - yii\base\ActionEvent, т.к. в любом контроллере присутствует код:public function имя_обработчика($event) { }
И так создадим обработчик, который закрывает доступ, создаёт переменную в сессии.public function beforeAction($action) { $event = new ActionEvent($action); $this->trigger(self::EVENT_BEFORE_ACTION, $event); return $event->isValid; } public function afterAction($action, $result) { $event = new ActionEvent($action); $event->result = $result; $this->trigger(self::EVENT_AFTER_ACTION, $event); return $event->result; }
Но как же универсальность, а если мы захотим прикрепить наше поведение на другое действие, неpublic function closeDoor(\yii\base\ActionEvent $event) { if ($event->action->id === 'interview') { \Yii::$app->session->set('interview-access-lock', 1); } }
interview
? Переделаем. Добавим в наше поведение переменную$actions
, которое будет следить на какие действия вешать "замок".
Аналогично сделаем обработчик, который будет проверять переменную в сессииclass AccessOnce extends Behavior { public $actions = []; public function closeDoor(\yii\base\ActionEvent $event) { if (in_array($event->action->id, $this->actions, true)) { \Yii::$app->session->set($event->action->id . '-access-lock', 1); } } }
При срабатыванииclass AccessOnce extends Behavior { public $actions = []; public $message = 'Доступ ограничен. Вы ранее совершали действия на этой странице.'; public function checkAccess(\yii\base\ActionEvent $event) { if (in_array($event->action->id, $this->actions, true)) { if (\Yii::$app->session->get($event->action->id . '-access-lock') !== null) { throw new HttpException(403, $this->message); } } } }
пользователя перекинет наthrow new HttpException(403, $this->message);
/index.php?r=site/error
. Самостоятельно попробуйте разобраться как сработаетsite/error
.
После всего сделанного, в итоге имеем:
Первый раз EVENT_BEFORE_ACTION пускает нас на страницу, так как не обнаруживает переменную в сессии. EVENT_AFTER_ACTION устанавливает эту переменную и при повторном заходе на страницу, EVENT_BEFORE_ACTION нас уже не пропускает. Чтобы изменить такое поведение, нужно отключить EVENT_AFTER_ACTION, если данные не корректные. Для этого отключаем поведение, черезclass SiteController extends Controller { public function behaviors() { return [ 'accessOnce' => [ 'class' => '\frontend\behaviors\AccessOnce', 'actions' => ['interview'] ], ]; } //... } class AccessOnce extends Behavior { public function events() { $owner = $this->owner; if ($owner instanceof Controller) { return [ $owner::EVENT_BEFORE_ACTION => 'checkAccess', $owner::EVENT_AFTER_ACTION => 'closeDoor', ]; } return parent::events(); } //... }
Controller::detachBehaviors
:
Теперь обработчик EVENT_BEFORE_ACTIONpublic function actionInterview() { $model = new Interview(); if ($model->load(Yii::$app->request->post())) { if ($model->validate()) { Yii::$app->session->setFlash( 'success', 'Спасибо, что уделили время. В ближайшее время будут опубликованы результаты.' ); return $this->redirect(Url::home()); } } $this->detachBehaviors('accessOnce'); return $this->render('interview', ['model' => $model,]); }
checkAccess
будет срабатывать каждый раз. А обработчикcloseDoor
события EVENT_AFTER_ACTION будет срабатывать, только когда сработаетreturn $this->redirect(Url::home());
, в противном случае поведение будет откреплено от контроллера и не сможет влиять на обработку события.
Осталось сохранить данные.
Таблицуinterview
в базе данных, которая описывает нашу модельfrontend/models/Interview
, мы создали ранее. Напомним, что использовали шаблон проектирования Active Record. МодельInterview
наследует всю функциональность из yii\db\ActiveRecord, поэтому трудностей с сохранением не должно возникнуть. Нужно использовать лишь один методsave($runValidation == true)
, который также включает в себя валидацию данных. Т.е. метод$model->validate()
мы можем заменить на$model->save()
в контроллереSiteController
.
Методpublic function actionInterview() { $model = new Interview(); if ($model->load(Yii::$app->request->post()) && $model->save()) { Yii::$app->session->setFlash( 'success', 'Спасибо, что уделили время. В ближайшее время будут опубликованы результаты.' ); return $this->redirect(Url::home()); } $this->detachBehaviors('accessOnce'); return $this->render('interview', ['model' => $model,]); }
save($runValidation)
подразумевает под собой следующий сценарий:
- вызывается
beforeValidate()
, если$runValidation = true
. Если$runValidation = false
этот и последующий шаги игнорируется. - вызывается
afterValidate()
. - вызывается
beforeSave()
. Если метод возвращает false, то процесс прерывается и дальнейшие шаги не выполняются. - происходит сохранение данных в базу данных
- вызывается
afterSave()
;
beforeSave()
в модели Interview:
public function beforeSave($insert) { if (parent::beforeSave($insert)) { $this->planets = implode(',', $this->planets); $this->astronauts = implode(',', $this->astronauts); return true; } return false; }
Дополнительная информация для самостоятельного ознакомления:
6 урок
Административное приложение Backend
В этом разделе добавим функциональности в backend приложение. Создадим место, где администратор сможет просматривать результаты опроса. Рассмотрим возможности ограничения доступа к тому или иному функционалу.
Чтобы начать, выполните команду из директории yii2-tutorial:
Входной файл административного раздела (далее Backend) доступен по ссылке /yii2-app-advanced/backend/web/index.php, а все файлы для работы backend располагаются в директорииgit checkout -f step-0.4
/yii2-app-advanced/backend/
.
Если вы не прошли аутентификацию на сайте, то в backend вас не пустит. В работу вступил так называемый фильтр контроллера yii\filters\AccessControl. Фильтры являются особым видом поведений, которые могут быть выполнены до действия контроллера или после.
Если открытьSiteController
backend части, то можно обнаружить следующий код:
С помощью правил доступаreturn [ 'access' => [ 'class' => AccessControl::className(), 'rules' => [ [ 'actions' => ['login', 'error'], 'allow' => true, ], [ 'actions' => ['logout', 'index'], 'allow' => true, 'roles' => ['@'], ], ], ], ];
rules
можно описать к каким действиям контроллера применять те или иные ограничения.
Подробная информация по работе фильтров описана в официальном руководствеTODO: https://github.com/yiisoft/yii2/blob/master/docs/guide-ru/security-authentication.md
Теперь давайте вернёмся к форме "Опрос". Для работы с формой в клиентской части(далее frontend) мы использовали Active Record модельInterview
, которая описывала форму. Т.к. эта модель описана в frontend, то в backend она не доступна. Чтобы исправить это, необходимо модель расположить в общей директории -common/models/
. Необходимо скопировать файлInterview.php
изfrontend/models
вcommon/models/
. Это уже сделано.
Вам осталось изменить файлы следующим образом. В common модели изменим пространство имени, удалим свойство "проверочный код", удалим правила, так как это всё требуется на стороне frontend части. А в frontend модели изменить родительский класс с\yii\db\ActiveRecord
на\common\models\Interview
и удалите методыtableName()
иattributeLabels()
.
Теперь, когда все изменения внесены, в backend возможно использовать модель\common\models\Interview
. Создадим вид, в котором будут отображаться все записи из таблицы "Опросов". Чтобы облегчить выполнение этой задачи, обратимся к Gii. Выберите генератор "CRUD Generator", который генерирует виды и контроллер на основании модели. Введите в Model Classcommon\models\Interview
, а в Controller Class -backend\controllers\InterviewController
. Всё, жмите Preview. "CRUD Generator" генерируется вид для создания, изменения, удаления и просмотра модели, также помогает генерировать страницуindex.php
, которая показывает список моделей постранично, используя виджет GridView или ListView Нажимаем "Generate" и наслаждаемся результатами работы.
Виджет GridView
Очень часто необходимо вывести данные в виде таблицы. Для решения этой задачи в Yii имеется сверхмощный виджет yii\grid\GridView. Разрабатывая административный раздел сайта, практически всегда этот виджет будет полезен. Вот и сейчас в видеyii2-app-advanced/backend/views/interview/index.php
он используется для того, чтобы отобразить все ответы на опрос.
Для работы этого виджета нужен объект<?= GridView::widget([ 'dataProvider' => $dataProvider, 'columns' => [ //... ], ]); ?>
$dataProvider
, который реализует интерфейс yii\data\DataProviderInterface Интерфейс можно разделить на следующие части: набор данных; объект, который отвечает за сортировку данных; объект, который отвечает за постраничную разбивку данных. Есть несколько реализаций этого интерфейса:
В нашем случае для моделей используется Active Record, поэтому и для удобства работы, лучше выбрать первый класс -yii\data\ActiveDataProvider
, так как это позволит представить набор данных в виде массива Active Record объектов. $dataProvider создаётся в контроллереyii2-app-advanced/backend/controllers/InterviewController.php
:
Interview::find() - подготавливает запрос типаnew ActiveDataProvider([ 'query' => Interview::find(), ]);
SELECT * FROM interview
. Далее $dataProvider передаётся в вид, где срабатывают внутренние механизмыActiveDataProvider
для отображения данных, в соответствии с разбивкой на страницы и сортировкой - выполняется запрос типа
Разбивка записей страницу и сортировка могут быть настроены следующим образом:SELECT * FROM `interview` ORDER BY имя_атрибута LIMIT количество_записей_на_страницу OFFSET (номер_страницы - 1) * количество_записей_на_страницу
Когда данные получены из базы данных, то с учётом настроек свойства$dataProvider = new ActiveDataProvider([ 'query' => Interview::find(), 'pagination' => [ 'pageSize' => 50, ], 'sort' => [ 'defaultOrder' => [ 'name' => SORT_ASC, ] ] ]);
columns
виджета GridView эти данные приобретают окончательный вид и выводятся на экран. Формат вывода данных может быть изменён путём изменения свойства columns:
Сейчас<?php $planets = ['Меркурий', 'Венера', 'Земля', 'Марс', 'Юпитер', 'Сатурн', 'Уран', 'Нептун']; $astronauts = [ 'Юрий Гагарин', 'Алексей Леонов', 'Нил Армстронг', 'Валентина Терешкова', 'Эдвин Олдрин', 'Анатолий Соловьев' ]; echo GridView::widget( [ 'dataProvider' => $dataProvider, 'columns' => [ ['class' => 'yii\grid\SerialColumn'], 'name', [ 'attribute' => 'sex', 'value' => function ($model) { return $model->sex ? 'Мужчина' : 'Женщина'; } ], [ 'attribute' => 'planets', 'value' => function ($model) use ($planets) { $result = null; $numbers = explode(',', $model->planets); foreach ($numbers as $number) { $result .= $planets[$number] . ' '; } return $result; } ], [ 'attribute' => 'astronauts', 'value' => function ($model) use ($astronauts) { $result = null; $numbers = explode(',', $model->astronauts); foreach ($numbers as $number) { $result .= $astronauts[$number] . ' '; } return $result; } ], [ 'attribute' => 'planet', 'value' => function ($model) use ($planets) { return $planets[$model->planet]; } ], [ 'class' => 'yii\grid\ActionColumn', 'template' => '{delete}', ], ], ] ); ?>
InterviewController
не использует фильтры для ограничения доступов к своим действиям. Попробуйте самостоятельно добавить условия, чтобы действияactionIndex
иactionDelete
могли выполнять только аутентифицированные пользователи. Остальные действия (создание, изменение, просмотр опроса)InterviewController
можно удалить за ненадобностью.
Дополнительная информация для самостоятельного ознакомления:
7 урок
Знакомство с тестированием
Неотъемлемой частью разработки масштабного приложения служит тестирование тех или иных частей этого приложения. Есть даже методика разработки приложений, которая тесно связана с тестированием - разработка через тестирование. Возможно, вы знакомы с ней как TDD.
Разработчики и сообщество Yii приложило много усилий, чтобы можно было максимально просто покрыть тестами необходимый код.
Раньше, например когда создавали форму опрос, приходилось открывать браузер, заполнять форму выдуманными данными, проверять результат. Добавляли успешный вывод сообщения, в виде результата - опять открывали форму, заполняли данными, проверяли результат. Добавили, поведение к форме - опять проверяли, открывая форму и вводя данные. Сохраняли результат в базе данных - приходилось смотреть сохранились ли данные корректно. Всё это наверное вам знакомо. Возможно, вам приходится такое проделывать, когда разрабатывается тот или иной функционал. А когда приложение становится масштабным, уже боязно вносить изменения в код. Так как далее приходится тратить много времени, чтобы пройтись по некоторым страницам сайта и проверить вручную всё ли работает как требуется. И часто, спустя несколько дней, кто-нибудь сообщает, что то что, когда-то работало, перестало работать. И опять тратиться время на выяснение причины неисправности, а так как изменения вносились несколько дней назад, то поиск истиной причины становится мукой. Или причина неисправности определяется неверно и в результате добавляется "костыль", который исправляет проблему.
Может вы к этому привыкли и вас всё устраивает. Но что, если про это всё забыть и использовать всего лишь одну команду:
Всё! Больше ничего. Запустив команду, после очередного изменения кода и не увидев ни одной ошибки в результатах, вы можете со спокойной душой сообщить всем, что всё работает, как того требует техническое задание.codecept run
В этой главе посмотрим, что скрывается под командойcodecept run
. Чтобы начать, выполните команду из директории yii2-tutorial:
git checkout -f step-1.0
Под Windows вместо стандартной командной строки cmd лучше использовать другой интерпретатор. Тот который использует подсветку кода, например Cmder Если у вас возникли проблемы с кодировкой в командной строке, то попробуйте выполнить в ней команду "chcp 65001".Codeception
Yii для тестирование кода предоставляет систему Codeception. Codeception основан на php фреймворке для тестирования PHPUnit. Вся настройка Codeception сводится к следующим шагам:
- создайте директорию
codecept
, где посчитаете нужным (не внутри учебника) - создайте в этой директории
composer.json
:{ "require": { "codeception/codeception": "*", "codeception/verify": "*", "codeception/specify": "*" } }
- запустите команду
composer install
из этой директории - после установки всех зависимостей, настройте переменную
PATH на директории
codecept\vendor\bin\
, чтобы команда codecept была доступна из любого места.
codecept -V
, чтобы увидеть версию Codeception и убедиться, что он успешно установлен.
Виды тестов
Codeception поддерживает следующие виды тестов:
- Модульные тесты (Unit), которые проверяют код, который выполняется в изоляции.
- Функциональные тесты (Functional), которые проверяют код, через эмуляцию браузера.
- Приёмочные тесты (Acceptance), которые проверяют код, через браузер.
\yii2-tutorial\yii2-app-advanced\tests\codeception
. Как и структура Advanced шаблона Yii, тесты также разделены для гибкости на backend и frontend. Только настройки вынесены в отдельную директориюconfig
.
Кстати для некоторых тестов необходимо соединение с базой данных. Придётся извлекать, записывать данные, а иногда и удалять. Понятное дело, что основную базу лучше в этом случае не использовать. Нужна тестовая. Используя миграции, не составит труда создать копию основной базы данных.
Поэтому вyii2-app-advanced/tests/codeception/config/config.php
указана:
К этой базе данных применены все миграции, которые использовались и для основной. Чтобы это выполнить, понадобилось вызвать'db' => [ 'dsn' => 'sqlite:' . dirname(__FILE__) .'/../../sqlite-test.db', ],
php yii migrate
изyii2-app-advanced/tests/codeception/bin
.
Теперь вернёмся к структуре директорий. Открывyii2-app-advanced/tests/codeception/backend
, можно обнаружить
[_output] [acceptance] [functional] [unit]
acceptance, functional, unit
директории, которые хранят тесты в зависимости от их видов._output
- это директория, в которую будет попадать результат эмуляции браузера (html код страницы) для функциональных тестов, в случае ошибки.
В Codeception есть понятие "Исполнители тестов". Из названия понятно, для чего они. Для того, чтобы их создать необходимо произвести их инициализацию. В директорииtests/codeception/frontend
необходимо выполнитьcodecept build
. Создадутся файлы:
которые и будут являться исполнителями. В последствии, тоже самое нужно проделать и для backend части.yii2-app-advanced/tests/codeception/frontend/acceptance/AcceptanceTester.php yii2-app-advanced/tests/codeception/frontend/functional/FunctionalTester.php yii2-app-advanced/tests/codeception/frontend/unit/UnitTester.php
Запуск тестов
Можно попробовать для frontend запустить тесты. Вyii2-tutorial\yii2-app-advanced\tests\codeception\frontend\
запустим на выполнение все функциональные тесты:
Выполнив эту команду, вы наверное ничего и не почувствовали. Но обратите внимание на время выполнения - 3 секунды. За эти три секундыcodecept run functional Time: 3 seconds, Memory: 29.25Mb OK (6 tests, 49 assertions)
FunctionalTester
успел посетить страницу о нас, обратная связь, домашнюю страницу и проверить что они работают без ошибок. В эти 3 секунды исполнитель побывал на странице регистрации, аутентификации, и на странице опроса, проверил эти формы, на корректность ввода данных. Исполнитель проверил работоспособность поведения accessOnce, которое было создано ранее. В сумме за три секунды исполнитель выполнил 6 тестов, в которых присутствовало 49 проверок.
Представьте сколько бы времени у вас ушло, если бы это выполняли самостоятельно через браузер. И запустив
после очередного рефакторинга кода, можно с уверенностью сказать, корректно ли работает сайт. А не бродить по сайту в поисках "А не поломал ли я чего-нибудь?". В этом и есть одна из приятных особенностей тестирования.codecept run functional
Также можно попробовать запустить модульные тесты:
А вот для приёмочных, вы обнаружите ошибки.codecept run unit Time: 8.29 seconds, Memory: 17.00Mb OK (9 tests, 25 assertions)
Всё дело в том, что для работы приёмочных тестов нужен браузер. Окружение для Codeception в Yii настроено таким образом, что используется PhpBrowser.codecept run acceptance FAILURES! Tests: 5, Assertions: 0, Errors: 5.
Т.е. настраивать ничего не нужно, остаётся изменить только путь к нашему сайту с http://localhost:8080 на http://localhost:8888/yii2-app-advanced/. Сделайте это.// Файл yii2-app-advanced/tests/codeception/frontend/acceptance.suite.yml modules: enabled: - PhpBrowser - tests\codeception\common\_support\FixtureHelper config: PhpBrowser: url: http://localhost:8080
Вместо PhpBrowser может быть выбран любой другой браузер (Chrome, FF, IE или другой) под управлением специального веб-драйвера, например Selenium. Смена PhpBrowser браузера позволит тестировать поведения в браузере, связанные с javascripts.
А сейчас, когда PhpBrowser настроен, можно пробовать запускать и приёмочные тесты.
Запуск отдельных тестов, сокращает время ожидания выполнения. Codeception поддерживает: запуск всех тестов, запуск тестов по видам и запуск отдельных тестов. Например, дляyii2-app-advanced/tests/codeception/frontend
можно выполнить:
codecept run codecept run functional codecept run functional functional\InterviewCept.php
Дополнительная информация для самостоятельного ознакомления:
8 урок
Работа с реляционными данными
Вы уже познакомились с тем как с помощью Active Record можно получить запись из базы данных. В этом разделе научимся получать связанные данные и работать с ними.
Чтобы начать, выполните команду из директории yii2-tutorial:
Сперва определимся, что мы хотим получить. Возьмём нашу солнечную систему. В солнечной системе есть звезда Солнце, вокруг звезды вращаются планеты - Меркурий, Венера, Земля, Марс, Церера, Юпитер, Сатурн, Уран, Нептун, Плутон, Хаумеа, Макемаке, Эрида; а вокруг планет их спутники. Для хранение этих данных нам понадобятся три таблицы: звезды, планеты, спутники.git checkout -f step-1.1
Star
Planet| id | name | |----|------| | | |
Satellite| id | name | star_id | |----|------|---------| | | | |
| id | name | planet_id | |----|------|-----------| | | | |
Хорошим тоном служит именовать таблицы в единственном числе на английском языке, например Planet, но не Planets. Внешние ключи принято называть в сочетании имени и поля таблицы, например "planet_id", а первичные ключи - "id". Подробнее...Создадим эти таблицы, через миграцию. Выполните вyii2-tutorial\yii2-app-advanced
:
Приведём код миграции к следующему виду:php yii migrate/create create_asto_tables Yii Migration Tool (based on Yii v2.0.3) Create new migration '/yii2-tutorial/yii2-app-advanced/console/migrations/m150513_054155_create_asto_tables.php'? (yes|no) [no]:yes New migration created successfully.
Обратите внимание, что имена таблиц имеют вид<?php use yii\db\Schema; use yii\db\Migration; class m150513_054155_create_asto_tables extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable( '{{%star}}', [ 'id' => Schema::TYPE_PK, 'name' => Schema::TYPE_STRING . ' NOT NULL', ], $tableOptions ); $this->createTable( '{{%planet}}', [ 'id' => Schema::TYPE_PK, 'name' => Schema::TYPE_STRING . ' NOT NULL', 'star_id' => Schema::TYPE_INTEGER . ' NOT NULL', 'FOREIGN KEY(star_id) REFERENCES ' . $this->db->quoteTableName('{{%star}}') . '(id) ON UPDATE CASCADE ON DELETE CASCADE' ], $tableOptions ); $this->createTable( '{{%satellite}}', [ 'id' => Schema::TYPE_PK, 'name' => Schema::TYPE_STRING . ' NOT NULL', 'planet_id' => Schema::TYPE_INTEGER . ' NOT NULL', 'FOREIGN KEY(planet_id) REFERENCES ' . $this->db->quoteTableName('{{%planet}}') . '(id) ON UPDATE CASCADE ON DELETE CASCADE' ], $tableOptions ); } public function down() { $this->dropTable('{{%satellite}}'); $this->dropTable('{{%planet}}'); $this->dropTable('{{%star}}'); } }
"{{%name}}"
. Это необходимо, если вы захотите использовать префикс в именах таблиц, который можно установить через конфигурацию компонентаdb
:
Так как наша миграция, может в будущем использоваться не только на SQLite, но и на Mysql, то для Mysql с помощью'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'sqlite:' . __DIR__ . '/../../sqlite.db', 'tablePrefix' => 'astro', ],
$tableOptions
устанавливаем кодировку иENGINE=InnoDB
, для работы с внешними ключамиFOREIGN KEY
. В SQLite по умолчанию проверка внешних ключей отключена. Для того, чтобы её включить необходимо выполнить команду:
Выполнять её требуется всякий раз, когда устанавливается соединение с базой данных. У класса, который в нашем случае отвечает за соединение, yii\db\Connection есть события:PRAGMA foreign_keys = ON;
EVENT_AFTER_OPEN
- срабатывает каждый раз, после установки соединения с БД.EVENT_BEGIN_TRANSACTION
- срабатывает каждый раз, перед началом транзакции.EVENT_COMMIT_TRANSACTION
- срабатывает каждый раз, после применении транзакции.EVENT_ROLLBACK_TRANSACTION
- срабатывает каждый раз, после отмены транзакции.
EVENT_AFTER_OPEN
функцию-обработчик, которая будет включать проверку внешних ключей в SQLite. Это можно сделать, через глобальную конфигурацию компонента. Добавьте к настройкам базы данныхon afterOpen
:
'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'sqlite:' . __DIR__ . '/../../sqlite.db', 'on afterOpen' => function ($event) { $event->sender->createCommand('PRAGMA foreign_keys = ON;')->execute(); } ],
Освежите знания о событиях в Yii 2Заметьте, что таким способом ('on имя_события'=>обработчик
) можно присоединять обработчики к любым событиям компонентов или приложений. Так же можно можно поступить и с поведениями. Например, запретить доступ "гостям" к методуlogout
в контроллереsite
, можно с помощьюas access
:
И так, когда все настройки применены, миграция создана, можно запустить на выполнение:'as access' => [ 'class' => 'yii\filters\AccessControl', 'rules' => [ [ 'controllers'=>['site'], 'actions' => ['logout'], 'allow' => true, 'roles' => ['@'], ], ] ]
Таблицы готовы.php yii migrate Yii Migration Tool (based on Yii v2.0.3) Total 1 new migration to be applied: m150513_054155_create_asto_tables Apply the above migration? (yes|no) [no]:yes *** applying m150513_054155_create_asto_tables > create table {{%star}} ... done (time: 0.059s) > create table {{%planet}} ... done (time: 0.041s) > create table {{%satellite}} ... done (time: 0.046s) *** applied m150513_054155_create_asto_tables (time: 0.204s) Migrated up successfully.
Создадим модели, через Gii.
Используя это изображение, остальные модели -Planet
иSatellite
, создайте самостоятельно. После в директорииyii2-app-advanced/common/models
появятся три файлаPlanet.php
,Star.php
,Satellite.php
, которые описывают модели.
Описание реляционных данных.
ОткрывStar.php
, вы обнаружите новый метод, с которым до сих пор мы не встречались:
Из названия/** * @return \yii\db\ActiveQuery */ public function getPlanets() { return $this->hasMany(Planet::className(), ['star_id' => 'id']); }
getPlanets
можно понять, что данный метод должен возвращать модели планет. Ну и в реализации, используется$this->hasMany(Planet...
, который обозначает, что звезда имеет много планет. Можно догадаться, что в моделяхPlanet.php
иSatellite.php
также должны быть похожие методы, которые описывают связи между моделями. Точно:
// в Planet.php public function getStar() { return $this->hasOne(Star::className(), ['id' => 'star_id']); } public function getSatellites() { return $this->hasMany(Satellite::className(), ['planet_id' => 'id']); }
Метод// в Satellite.php public function getPlanet() { return $this->hasOne(Planet::className(), ['id' => 'planet_id']); }
hasOne
из ActiveRecord в отличиеhasMany
обозначает отношение моделей, как связь один к одному. Например спутник Луна принадлежит только одной планете Земля.
Доступ к реляционным данным.
Из описания методов, которые описаны выше, можно увидеть, что возвращается объект \yii\db\ActiveQuery.
Для того, что получить все спутники для планеты Марс, нужно обратиться к коду:/** * @return \yii\db\ActiveQuery */ public function getPlanet() {..}
Например, у Юпитера 67 спутников, а нужно получить только 10 первых, которые отсортированы по имени:$marsModel = Planet::find()->where(['name'=>'Марс'])->one(); $marsModel->getSatellites()->all();
Результатом будет массив Active Record моделей. Иногда, для экономии памяти, результат стоит возвращать в виде массива значений с помощью$marsModel = Planet::find()->where(['name'=>'Юпитер'])->one(); $marsModel->getSatellites()->limit(10)->orderBy(['name'=>SORT_ASC])->limit(10)->all();
->asArray()->all()
.
Эти примеры выполняли в два этапа: находилась модель, находились отношения, если в этом была необходимость. Можно сделать тоже самое в один запрос:
Уже упоминалось, что почти каждый класс в Yii наследует$marsModel = Planet::find()->with('satellites')->where(['name'=>'Юпитер'])->one();
yii\base\Object
. Это означает, что к любому методу, который начинающийся как get(геттер) или set(сеттер), может быть использован как свойство объекта. Т.е.getPlanet()
в моделиSatellite
может быть получено как$satelliteModel->planet
:
эквивалентно$marsModel = Planet::find()->where(['name'=>'Марс'])->one(); $marsModel->getSatellites()->all();
$marsModel = Planet::find()->where(['name'=>'Марс'])->one(); $marsModel->satellites; //вернёт массив Active Record моделей Satellites
Дополнительная информация для самостоятельного ознакомления:
9 урок
Вывод реляционных данных в видах.
Сгенерируйте через CRUD Gii. виды и контроллер для моделейStar
,Planet
иSatellite
. В качестве подсказки воспользуйтесь следующим изображением:
Обратите внимание, что моделиStar
,Planet
иSatellite
мы располагаем в пространстве имёнcommon\models
, которое подразумевает доступность моделей из frontend и backend. А вспомогательные модели для фильтрации и сортировкиSearch Model Class
в пространстве вbackend\models
, т.к. определённая фильтрация и сортировка понадобится только в backend приложении.
Чтобы убедиться в правильности выполненных действий выполните тесты.
Изyii2-tutorial\yii2-app-advanced\tests\codeception\bin
установите миграцию для тестовой базы
Изphp yii migrate
yii2-tutorial\yii2-app-advanced\tests\codeception\backend
выполните две команды для запуска тестов:
- создайте исполнителей для тестов
codecept build
- запустите функциональный тест AstroCept
После того, как Gii сгенерировал контроллеры и виды, станут доступны следующие url:codecept run functional functional\AstroCept.php Time: 519 ms, Memory: 21.00Mb OK (1 test, 3 assertions)
Эти страницы служат интерфейсом, отправной точкой для работы с моделями. С этих страниц можно попасть на формы для создания или изменения информации по звёздам, планетам и их спутникам. На данный момент база данных не содержит какой-либо информации по звёздам, планетам и их спутникам. Перейдите на следующий шаг, в котором в базу данных добавлена эта информация:
Обновив страницу управления планетами, можно увидеть информацию:git checkout -f step-1.2
Давайте приведём её к более красивому виду. Для настройки вида откройте файлyii2-app-advanced/backend/views/planet/index.php
Вначале изменитеtitle
компонентаView
:
Измените название ссылки$this->title = 'Планеты';
Ну и наконец измените структуру колонок в таблице на:<?= Html::a('Добавить планету', ['create'], ['class' => 'btn btn-success']) ?>
тут мы определили пять колонок:'columns' => [ 'id', [ 'attribute'=>'name', 'label'=>'Планета', ], [ 'label'=>'Звезда', 'attribute'=>'star_id', 'value' => function($planet) { return $planet->star->name; } ], [ 'label'=>'Количество спутников', 'value' => function($planet) { return $planet->getSatellites()->count(); } ], ['class' => 'yii\grid\ActionColumn'], ],
- первая
id
, служит для отображения служебной информации по id записям планет. - вторая
name
в виде массиваattribute
, который указывает, что значение для колонки требуется брать из атрибутаname
, но заголовок у этой колонки по умолчаниюName
(Наименование). Изменим на'label'=>'Планета',
- третья колонка содержала числовое значение
star_id
, что не совсем удобно. Так как у модели Planet настроена связь
, то значение колонкиpublic function getStar() { return $this->hasOne(Star::className(), ['id' => 'star_id']); }
value
можно изменить на анонимную функцию, которая будет возвращать наименование звезды.
- четвертая колонка будет отображать количество спутников у планеты. Только в анонимной функции мы обратились непосредственно
к методу
Planet::getSatellites()
, так как$planet->star
через магический метод__get
получает массив моделей, который в данном пункте избыточен, в отличие от предыдущего. - ну и пятая колонка ActionColumn, выводит три ссылки, для стандартных операций с моделью Planet (просмотр, редактирование, удаление).
- была также колонка yii\grid\SerialColumn, которая служит для вывода порядкового номера строки. Мы её удалили за ненадобностью.
Star
(/views/star/index.php
) иSatellite
(/views/satellite/index.php
).
Вы наверное уже обратили внимание, что только у колонки "Количество спутников" отсутствует поле для фильтрации, а также недоступна сортировка. Всё потому, что мы не указали свойствоattribute
для этой колонки. Исправим это:
Но обновив страницу ничего не изменится - мало указать[ 'label'=>'Количество спутников', 'attribute'=>'countSatellites', 'value' => function($planet) { return $planet->getSatellites()->count(); } ],
attribute
, нужно указать, что это свойство является безопасным, вrules()
модели для поиска, т.е. в/backend/models/SearchPlanet.php
.
Теперь на странице появилось поле input для фильтрации, но фильтрация и сортировка по этому полю по-прежнему не работает. Осталось настроитьclass SearchPlanet extends Planet { public $countSatellites; /** * @inheritdoc */ public function rules() { return [ [['id', 'star_id', 'countSatellites'], 'integer'], [['name'], 'safe'], ]; }
$dataProvider
, а именно свойство$sort
и$query
yii\data\ActiveDataProvider. Сортировка и фильтрация выполняется путём добавления к sql запросуORDER BY
илиWHERE
. Т.е. когда сработает сортировка по полюcountSatellites
, то должен выполнится запрос
Так как в таблице Planet нету поляSELECT * FROM Planet ORDER BY countSatellites ASC|DESC;
countSatellites
, то такой запрос не выполнится. Поэтому нужно изменить запрос, чтобы в нём участвовалcountSatellites
. На данный момент в методеsearch()
моделиbackend\models\SearchPlanet
у нас:
Нам нужно изменить его так, чтобы в запросе участвовало$query = Planet::find(); // эквивалентно выполнению SELECT * FROM Planet
countSatellites
:
Когда есть sql запрос, то не составит труда его переделать вSELECT planet.*, count(planet_id) as countSatellites FROM planet LEFT JOIN satellite ON planet_id = planet.id GROUP BY planet.id ORDER BY countSatellites ASC|DESC;
$query
(ActiveQuery):
Теперь сортировка работает для всех полей. Проверьте - управление Planet. Осталось настроить фильтрацию для поля$query = Planet::find() ->select([$this->tableName() . '.*', 'count(planet_id) as countSatellites']) ->joinWith('satellites') ->groupBy($this->tableName() . '.id');
countSatellites
. Так в sql запросеcountSatellites
- это агрегатные функцияCOUNT()
, то для неё параметр запросаWHERE
не сработает, необходимHAVING
. Для ActiveQuery это эквивалентно вызову метода$query->having()
. Но параметрHAVING
понадобится только, когда поле фильтра будет заполнено. С учётом всего этого методsearch()
моделиbackend\models\SearchPlanet
примет следующий вид:
Для закрепления знаний, настройте фильтрацию и сортировку для дополнительного свойстваpublic function search($params) { $query = Planet::find() ->select([$this->tableName() . '.*', 'count(planet_id) as countSatellites']) ->joinWith('satellites') ->groupBy($this->tableName() . '.id'); $dataProvider = new ActiveDataProvider( [ 'query' => $query, 'sort' => [ 'attributes' => [ 'id', 'name', 'star_id', 'countSatellites' => [ 'asc' => ['countSatellites' => SORT_ASC,], 'desc' => ['countSatellites' => SORT_DESC,], ], ] ] ] ); $this->load($params); if (!$this->validate()) { return $dataProvider; } if ($this->countSatellites) { $query->having(['countSatellites' => (int) $this->countSatellites]); } $query->andFilterWhere( [ $this->tableName() . '.id' => $this->id, 'star_id' => $this->star_id, ] ); $query->andFilterWhere(['like', 'name', $this->name]); return $dataProvider; }
countPlanets
в моделиStarSearch
на странице управления моделями Star.
10 урок
Сохранение реляционных данных.
Формы для сохранения данных
Вы наверное уже обратили внимание на формы для сохранения:
Сейчас они выглядит, мягко говоря, не удобно для того, чтобы ими пользоваться.
Давайте начнём с первой формы - сохранение информации по звёздам. Чтобы найти файл формы - смотрим на urlstar/create
, далее открываем контроллерStarController
, ищем в нём методactionCreate()
. Видим, что вызывается видcreate
. Следовательно открываемyii2-app-advanced/backend/views/star/create.php
и обнаруживаем, что в этом файле нет нам уже знакомого классаActiveForm
для работы с формами. А есть:
Как уже известно в видах $this - это yii\web\View. Метод$this->render('_form', ['model' => $model,])
render
, вам встречался в контроллере, но там его реализация отличается тем, что до вывода вида, вызывается компонент для работы с видами, т.е. вызываетсяyii\web\View
и только затем его метод для получения вида из файла. Тут же$this
это уже компонент для работы с видами и егоrender()
получает вид_form
. Получается, что один вид_form
находится внутри другого видаcreate
. Почему это так? Это удобно, так как вид_form
содержит форму, которая может быть использована не только для создания модели, но и также для изменения уже существующей модели. Если открыть видupdate.php
в этой же директории то, можно обнаружить тот же код, что в иcreate.php
:
Т.е. одна форма используется для видов$this->render('_form', ['model' => $model,])
create.php
иupdate.php
.
Открывyii2-app-advanced/backend/views/star/_form.php
вы не обнаружите ничего нового. Обычная форма - одно поле и кнопка. В браузере эта форма занимает почти всю ширину экрана. Не совсем красиво. У ActiveForm, что из пространства имёнyii\bootstrap\
есть свойство$layout
, которое может иметь значение['default', 'horizontal', 'inline']
. Попробуйте установитьinline
. Вviews/star/_form
:
Не забудьте изменить<?php $form = ActiveForm::begin(['layout'=>'inline']); ?>
yii\widgets\ActiveForm;
на корректное пространство. Обновите страницу с формой и посмотрите на результат. Красивее, чем было, хотя о вкусах не спорят. Приinline
можно заметить, что метки (label) не используются. Но есть placeholder. Добавим его кtextInput
:
С Yii версии 2.0.3<?= $form->field($model, 'name')->textInput(['maxlength' => 255, 'placeholder'=>'Введите название звезды']) ?>
maxlength
- максимальная длина введённого текста, может быть автоматически высчитана из правила валидации:
Для этого используйте:public function rules() { return [ [['name'], 'required'], [['name'], 'string', 'max' => 255] ]; }
Теперь наша форма готова. Введите название для звезды и нажав кнопку создать, почувствовать себя властелином Вселенной.<?= $form->field($model, 'name')->textInput(['maxlength' => true, 'placeholder'=>'Введите название звезды']) ?>
Открыв\backend\controllers\StarController::actionCreate
вы увидите уже знакомый принцип сохранения данных - проверка и дальнейшее их сохранение. Со звездой всё просто. Перейдём к планетам.
На форме с планетами появляется новое поле -star_id
. Тот, кто будет пользоваться этой формой, будет вспоминать программиста не добрым словом. Всех id не упомнишь, да и ошибиться всегда можно. Давайте сделаем выпадающий список с названиями звёзд.
Как мы делали когда-то для планет:
Только вместо массива будем использовать запрос<?= $form->field($model, 'planet')->dropDownList( ['Меркурий', 'Венера', 'Земля', 'Марс', 'Юпитер', 'Сатурн', 'Уран', 'Нептун'] ) ?>
Star::find()->all()
, который вернёт массив моделей.
Но можно переписать этот код с использованием класса помощника yii\helpers\ArrayHelper, который позволяет обращаться с массивами более эффективно:$stars = []; foreach (Star::find()->all() as $star){ $stars[$star->id] = $star->name; } echo $form->field($model, 'planet')->dropDownList($stars);
Конечно, вы можете обойтись без переменной$stars = ArrayHelper::map(Star::find()->all(), 'id', 'name'); echo $form->field($model, 'planet')->dropDownList($stars);
$stars
, записав этот код одну строку. Ну и после всего, для этой формы попробуйте использоватьhorizontal
:
<?php $form = ActiveForm::begin(['layout' => 'horizontal',]); ?>
Осталось форма создания спутников. Вы уже всё умеете, чтобы внести изменения самостоятельно.
Так как формы, для редактирования существующих моделей используются одни и те же, что и для сохранения. То, что-либо новое создавать не нужно.
Проверка работоспособности формы через тест
В этом подразделе описывается как написать функциональный тест для формы. Такой тест упростит отладку и разработку формы. Возможно, вам легче использовать браузер и по сто пятьдесят раз выдумывать и вводить данные, для того, чтобы проверить сохранение данных через форму, после очередной правки кода. Если это так, то можете пропустить этот подраздел и перейти дальше. Остальным добро пожаловать.
Будем двигаться небольшими шагами, чтобы было понятнее и легче.
Создадим функциональный тестPlanetFormCept
, который будет проверять работу формы для сохранения данных по планетам.
cd yii2-app-advanced\tests\codeception\backend\ codecept build
Откройте созданный файлcodecept generate:cept functional PlanetFormCept Test was created in ...
PlanetFormCept.php
и измените его содержимое на:
Можно запустить этот тест:<?php use tests\codeception\backend\FunctionalTester; /* @var $scenario Codeception\Scenario */ $I = new FunctionalTester($scenario); $I->wantTo('ensure than create form works');
Как видно запустился 1 тест и выполнилось 0 проверок. Теперь к тесту добавим команду на открытие страницы с формой. ля этого нужно создать объект этой страницы. В директорииcodecept run functional functional/PlanetFormCept.php Time: 1.51 seconds, Memory: 13.25Mb OK (1 test, 0 assertions)
yii2-app-advanced/tests/codeception/backend/_pages/
создайтеPlanetFormPage.php
с содержимым:
Теперь в нашем тесте можно воспользоваться этим объектом, для того, чтобы имитировать открытие страницы с формой:<?php namespace tests\codeception\backend\_pages; use yii\codeception\BasePage; class PlanetFormPage extends BasePage { public $route = 'planet/create'; }
Запускаем тест для того, чтобы убедиться, что всё выполнили правильно://... $I->wantTo('ensure than create form works'); $formPage = \tests\codeception\backend\_pages\PlanetFormPage::openBy($I);
На данный момент форма содержит два поля: тестовое поле "Название планеты" и выпадающий список "Название звезды". Проверим их через тест. Для этого нам понадобятся методыcodecept run functional functional/PlanetFormCept.php Time: 518 ms, Memory: 18.00Mb OK (1 test, 0 assertions)
fillField
иclick
из классаyii2-app-advanced/tests/codeception/backend/functional/FunctionalTester.php
, который создался с помощью ранее выполненной командыcodecept build
.
Ознакомьтесь с информацией по доступным методам модуля Yii2 для Codeception.<?php use tests\codeception\backend\FunctionalTester; /* @var $scenario Codeception\Scenario */ $I = new FunctionalTester($scenario); $I->wantTo('ensure than create form works'); $formPage = \tests\codeception\backend\PlanetFormPage::openBy($I); $I->fillField('//*[@id="planet-name"]','Новая Земля'); $I->selectOption('//*[@id="planet-star_id"]', 'Солнце'); $I->click('//*[@id="w0"]/div[3]/button'); $I->dontSeeInTitle('Новая планета');
- fillField - заполняем текстовое поле "Название планеты"
- click - нажимаем на кнопку "Создать"
- dontSeeInTitle - проверяем, чтобы в заголовке странице не было текста "Новая планета"
//*[@id="planet-name"]
и'//*[@id="w0"]/div[3]/button'
- это XPath. Например, в браузере Chrome, нажав на странице с формой F12, можно получить XPath через контекстное меню к html коду элемента:
Запустив тест, увидим ошибку:
Всё потому, что используется тестовая база данных, которая никакой информации по звёздам не содержит. Данные есть только в главной базе данных, но её использовать не будем, во избежание порчи данных. На помощь приходят фикстуры. Это состояние базы данных, до которого она будет доведена при запуске теста. Для работы с фикстурами исполнитель функциональных тестовcodecept run functional functional/PlanetFormCept.php InvalidArgumentException: Input "Planet[star_id]" cannot take "Солнце" as a value (possible values: ). 3. I select option "//*[@id="planet-star_id"]","Солнце" //...
FunctionalTester.php
использует класс помощникFixtureHelper.php
(в файлеtests/codeception/backend/functional.suite.yml
):
Откройте файл помощникclass_name: FunctionalTester modules: enabled: - Filesystem - Yii2 - tests\codeception\common\_support\FixtureHelper config: Yii2: configFile: '../config/backend/functional.php'
FixtureHelper.php
и найдите его метод:
Запуская каждый раз любой функциональный тест из backend, запускаетсяpublic function fixtures() { return [ 'user' => [ 'class' => UserFixture::className(), 'dataFile' => '@tests/codeception/common/fixtures/data/init_login.php', ], ]; }
FixtureHelper
, который загружает фикстуруUserFixture
:
, которая очищает таблицу для моделиclass UserFixture extends ActiveFixture { public $modelClass = 'common\models\User'; }
common\models\User
, а затем заполняет её данными изdataFile
.
Добавим новые фикстуры, которые будут сбрасывать состояние таблицы для звёзд, планет и их спутников. Создайте вyii2-app-advanced/tests/codeception/common/fixtures
файлы:
- PlanetFixture.php
- SatelliteFixture.php
- StarFixture.php
SatelliteFixture.php
:
Код её простой, остальные две:<?php namespace tests\codeception\common\fixtures; use yii\test\ActiveFixture; class SatelliteFixture extends ActiveFixture { public $modelClass = 'common\models\Satellite'; }
PlanetFixture
иStarFixture
создайте самостоятельно.
Без$dataFile
фикстуры ActiveFixture будут очищать таблицы без внесения первоначальных данных. Явно можно не указывать расположение файла с данными($dataFile
), а просто его создать в той же директории, где лежит фикстура, с учётом имени таблицы. Т.е. для фикструрыSatelliteFixture
нужно создать в директорииyii2-app-advanced/tests/codeception/common/fixtures/data
файл с именемsatellite.php
. Для вашего удобства эти файлы уже созданы заранее.
Фикстуры созданы, теперь нужно определить порядок их загрузки. Если мы сначала начнём загружать фикстуру для таблицы планет, то споткнёмся на ограничение внешних ключей в базе данных, т.е. вставляя данные изyii2-app-advanced/tests/codeception/common/fixtures/data/planet.php
получим ошибкуreturn [ [ 'name' => 'Земля', 'star_id' => '1', ], ];
SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed
которая обозначает, что звезды с ID = 1 не найдено. Поэтому сначала нужно загрузить фикстуру для звезды, затем для планеты и на последок фикстуру для спутников. Подключаем загрузку фикстур в файле помощнике FixtureHelper:
public function fixtures() { return [ 'user' => [ 'class' => UserFixture::className(), 'dataFile' => '@tests/codeception/common/fixtures/data/init_login.php', ], 'star' => [ 'class' => tests\codeception\common\fixtures\StarFixture::className(), ], 'planet' => [ 'class' => tests\codeception\common\fixtures\PlanetFixture::className(), ], 'satellite' => [ 'class' => tests\codeception\common\fixtures\SatelliteFixture::className(), ], ]; }
У ActiveFixture есть свойство $depends, с помощью которого можно также установить порядок связей фикстур.Фикстуры созданы, при запуске теста таблицы будут очищаться и заполняться данными из файловyii2-app-advanced/tests/codeception/common/fixtures/data/
. Поэтому теперь при запуске теста формы, мы сможем выбрать звезду из выпадающего списка:
Если посмотреть в контроллерcodecept run functional functional/PlanetFormCept.php Tests\codeception\backend.functional Tests (1) Trying to ensure than create form works Ok Time: 1.03 seconds, Memory: 21.50Mb OK (1 test, 1 assertion)
yii2-app-advanced/backend/controllers/PlanetController.php
, то в методеactionCreate
можно увидеть, что после сохранения происходит переход на действиеview
В конце нашего теста указано:return $this->redirect(['view', 'id' => $model->id]);
что не совсем корректно, так как данные могут не сохраниться, появится ошибка, но в этом случае$I->dontSeeInTitle('Новая планета');
dontSeeInTitle
вернёт утвердительный результат и тест выполнится успешно. Лучше заменить эту проверку на:
Теперь мы можем смело вносить изменения в код формы и запуская тест видеть, что всё работает корректно. Причём затраты по времени на проверку составит всего около 1 секунды, в то время как раньше, когда вы проверяли форму самостоятельно через браузер, уходило куда больше времени. Для закрепления основ самостоятельно напишите тесты к формам для сохранения звёзд и спутников.$I->seeInTitle('Новая Земля');
Сохранение реляционных данных.
У нас есть три формы для трёх разных моделей. Представьте себе ситуацию: нужно ввести информацию по новой планете, но звезды у неё ещё нету. Не совсем удобно переключаться с формы на форму, сохраняя новые данные. Давайте объединим работу с тремя формами в одной. Для этого нам понадобится новая модель формы, которая будет объединять работу с тремя моделями относительно модели Планет.
Дополнительная информация для самостоятельного ознакомления:
Комментарии
Отправить комментарий