Учебник yii2

1 урок

Создание сайта с использованием Yii 2.x

В данном учебнике описывается процесс создания сайта. Каждый шаг разработки описан максимально подробно и может быть применён при создании других приложений. В дополнение к полному руководству и API, данное пособие показывает, вместо полного и подробного описания, пример практического применения фреймворка Yii.
Для того, чтобы выполнять упражнения из учебника понадобятся инструменты composer и git. Не отчаивайтесь, если вам не известны эти инструменты, нужно будет лишь выполнить несколько команд, которые будут описаны.
Разработчики данного интерактивного курса:
Сообщество Yii

Начальная установка

Установим стартовый шаблон приложения [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
    git checkout -f step-0
    
    В последствии будет установлен "Шаблон приложения advanced", который станет доступен по ссылке.
    Пожалуйста, ознакомьтесь с официальным руководством, для того чтобы иметь представление, как устроен "Шаблон приложения 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
    git checkout -f step-0.1
    
    Перейдите по ссылке вы попадёте на статическую страницу "About".
    Если посмотреть на адрес ссылки, то можно увидеть 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, которое отвечает за заголовок открытой страницы. Также этот заголовок передаётся в "Навигационную цепочку", через
    $this->params['breadcrumbs'][] = $this->title;
    
    Попробуйте поменять заголовок "About" на текст "О нас!" и откройте страницу.

    Видно, что в меню, всё ещё осталось "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
      NavBar::begin([
                    'brandLabel' => Yii::$app->name,
    
    и чуть ниже в footer:
    <p class="pull-left">&copy; <?= 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, в котором уже имеется:
    'page' => [
        'class' => 'yii\web\ViewAction',
    ],
    
    По умолчанию в Advanced этого кода нет, он добавлен для вашего удобства. Теперь нужно перейти по адресу index.php?r=site/page&view=about
    Попробуйте поменять в адресной строке параметр view на duty или delivery. Проанализируйте результаты.

    4 урок

    Работа с формами

    В этом разделе рассмотрим как создать форму.
    Чтобы начать, выполните команду из директории yii2-tutorial
    git checkout -f step-0.2
    
    Как выглядит форма, созданная с помощью Yii2, можно увидеть по ссылке.
    Иногда можно увидеть на этой странице ошибку
    "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.
    class ContactForm extends Model
    {
        public $name;
        public $email;
        public $subject;
        public $body;
        public $verifyCode;
        
        public function rules()
        {
        
        }
        
        public function attributeLabels()
        {
        
        }
        
        public function sendEmail($email)
        {
        
        }
    
    Сначала перечисляются все атрибуты модели, это - имя пользователя($name), электронный адрес($email), тема сообщения($subject), само сообщение($body) и CAPTCHA(verifyCode). Эти атрибуты описывают модель - сущность "обратная связь". Дальше идёт метод rules(), который используется для валидации, проверки атрибутов модели. Второй метод attributeLabels используется для описания меток, маркировок атрибутов на понятном для человека языке. Обычно эти метки используются в представлении для описания элементов форм или другого. Следующий метод sendEmail отвечает за отправку "обратной связи" на электронный адрес администратора сайта.

    Как всё работает? (упрощённо)

    Запрос от пользователя поступает на входной скрипт web/index.php. В скрипте создаётся приложение yii\web\Application с учётом конфигураций. Приложение определяет маршрут - контроллер и действие. Создаётся экземпляр контроллера и вызывается действие. В действии создаёт модель и контроллер передаёт её в вид. Далее генерируется конечный ответ, с учётом шаблонов, видов и данных из моделей. Ответ отдаётся пользователю. Пользователь вводит данные и отправляет их опять в входной скрипт. Всё повторяется до контроллера. Теперь в контроллере опять создаётся модель, но перед отправкой её в представление, она наполняется данными с помощью метода yii\base\Model->load() и затем данные проверяются методом yii\base\Model->validate():
    public function actionContact()
    {
        $model = new ContactForm();
        if ($model->load(Yii::$app->request->post()) && $model->validate()) 
    
    Модель наполняется пользовательскими данными с помощью компонента request 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 нет, то атрибут модели считается не безопасным.
    public function rules()
    {
        return [       
            [['name', 'email', 'subject', 'body'], 'required'],       
            ['email', 'email'],      
            ['verifyCode', 'captcha'],
        ];
    }
    
    Видно, что все возможные атрибуты модели встречаются в коде rules, поэтому в данном случае они все являются безопасными. Т.е. для них выполняется условие:
    имя_модели[атрибут] = данные[имя_элемента_формы][атрибут] 
    
    Метод $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 реализован популярный способ доступа к данным Active Record - класс yii\db\ActiveRecord Этот класс наследуется от базового класса yii\base\Model, расширяя его до такого состояния, при котором модель становится отражением строки в таблице из базы данных. Следовательно с моделью можно работать так же как со строкой в базе данных - искать, создавать, изменять, удалять. Следующий код иллюстрирует всю мощь и красоту реализованного шаблона проектирования Active Record в Yii:
    $model = new ActiveRecord;
    $model->attributes = ['text' => 'Длинный текст', 'title' => 'Заголовок'];
    $model->save();
    
    С помощью трёх строк можно наполнить модель данными, проверить данные и в случае корректности, сохранить их в базу данных. Заметьте SQL не использовался, этот код сработает и для используемой нами SQLite и для любой другой СУБД.
    И так вернёмся к форме "Опрос". Создадим для начала таблицу в базе данных.
    Напомним, что для обращения к базе данных используется компонент, который мы настроили в 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. Применим новую миграцию:
    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.
    
    Таблица в базе данных создана. Теперь, чтобы использовать Active Record, необходимо создать модель, как отражение строки из СУБД. Чтобы облегчить эту задачу, в Yii есть замечательный инструмент Gii, который генерирует код.

    Gii - магический инструмент, который может написать код за вас.

    Gii включен в Advanced шаблоне приложения, если это приложение инициализировано в режиме отладки, т.е. как было ранее сделано, через
    php init --env=Development
    
    Чтобы попасть в Gii нужно перейти по ссылке index.php?r=gii и выбрать пункт Model Generator.
    Если ваш сайт установлен не на локальном хосте, то скорее всего вы увидите на странице 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';
    
    на
    $config['modules']['gii'] = [
        'class' => 'yii\gii\Module',
        'allowedIPs' => ['192.168.0.*']
    ];
    
    Теперь на страницу index.php?r=gii разрешено будет заходить только с тех устройств, которые находятся в подсети 192.168.0.0/24
    Вернёмся к Model Generator. Этот раздел Gii предназначен для генерации моделей. Для того, чтобы форма была сгенерирована, необходимо указать:
    • имя таблицы
    • имя будущей модели Interview
    • пространство имени frontend\models
    остальные поля оставляем как есть. Нажмём кнопку Preview (предпросмотр) и посмотрите models\Interview.php будущий код. После этого нажмите Generate. Всё наша модель создана и доступна по /yii2-app-advanced/frontend/models/Interview.php. Gii всё же не всесилен и потребуется внести некоторые изменения в модель. Добавим элемент "проверочный код" - verifyCode, как свойство модели:
    class Interview extends \yii\db\ActiveRecord
    {  
        public $verifyCode;
        
        //...
    }
    
    изменим метки для будущих элементов:
    public function attributeLabels()
    {
        return [
            'name' => 'Имя',
            'sex' => 'Пол',
            'planets' => 'Какие планеты обитаемы?',
            'astronauts' => 'Какие космонавты известны?',
            'planet' => 'На какую планету хотели бы полететь?',
            'verifyCode' => 'Проверочный код',
        ];
    }
    
    И так модель формы готова, сделаем саму форму. Опять обратимся за помощью к Gii, только теперь выберем генератор Form Generator, в котором следует указать:
    • имя вида (View Name) - site/interview
    • имя модели с учётом пространства имён - frontend\models\Interview
    Все остальные поля оставим как есть. Нажмём кнопку Preview, а затем Generate - создастся вид в директории views/site/interview.php. Также Gii предложит код действия для контроллера, который необходимо самостоятельно вставить в контроллер SiteController. Вот чуть измененный код действия:
    public function actionInterview()
    {
        $model = new \frontend\models\Interview();
        
        if ($model->load(Yii::$app->request->post())) {
            if ($model->validate()) {
                // делаем что-то, если форма прошла валидацию
                return;
            }
        }
        
        return $this->render('interview', [
            'model' => $model,
        ]);
    }
    
    Итак модель, контроллер с действием и представление созданы, теперь можно посмотреть на результат - index.php?r=site/interview

    Настройка вида формы

    Изменим вид формы на

    • Вид элемента name остаётся неизменным.
    • Вид элемента sex необходимо переделать на два переключателя. По умолчанию $form->field() генерирует текстовый <input type="text">. Это можно изменить следующим образом:
       <?= $form->field($model, 'sex')->radioList(['Мужчина', 'Женщина']) ?>
      
      ActiveForm->ActiveField->radioList() - метод в качестве первого элемента принимает массив возможных значений. Так как метка атрибута sex в модели определена как 'sex' => 'Пол', а необходимо "Вы мужчина/женщина?". То можно изменить "пол" на "вы", что не совсем понятно, если эти метки в других местах (например в письме, отчётных таблицах или в прочем). Поэтому сделаем это только в виде, с помощью ActiveField->label().
    <?= $form->field($model, 'sex')->radioList(['Мужчина', 'Женщина'])->label('Вы:') ?>
    
    • Дальше идёт список флаги(checkbox). Для генерации их используем метод ActiveField->checkboxList().
    <?= $form->field($model, 'planets')->checkboxList(
        ['Меркурий', 'Венера', 'Земля', 'Марс', 'Юпитер', 'Сатурн', 'Уран', 'Нептун']
    )->label('Какие планеты по вашему мнению обитаемы?') ?>
    
    • Дальше список с множественным выбором(select) и подсказкой(hint) - ActiveField->dropDownList():
    <?= $form->field($model, 'astronauts')->dropDownList(
        [
            'Юрий Гагарин',
            'Алексей Леонов',
            'Нил Армстронг',
            'Валентина Терешкова',
            'Эдвин Олдрин',
            'Анатолий Соловьев'
        ],
        ['size' => 6, 'multiple' => true]
    )
    ->hint('С помощью Ctrl вы можете выбрать более одного космонавта')
    ->label('Какие космонавты вам известны?') ?>
    
    ActiveField->hint() - формирует подсказку для элемента формы.
    • Дальше выпадающий список с одиночным выбором - метод ActiveField->dropDownList():
    <?= $form->field($model, 'planet')->dropDownList(
        ['Меркурий', 'Венера', 'Земля', 'Марс', 'Юпитер', 'Сатурн', 'Уран', 'Нептун']
    ) ?>
    
    • И виджет каптчи, как элемент формы:
    <?= $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('Нажмите на картинку, чтобы обновить.') ?>
    
    Html шаблона у каптчи формируется исходя из свойства 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'],
        ];
    }
    

    Дополнительная информация для самостоятельного ознакомления:


    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, так же, как настраивали язык и имя нашего приложения:
    return [
        'name' => 'Мой сайт',
        'language' => 'ru',
        'homeUrl' => ['/site/interview'],
    ]
    
    Url формируется как id контроллера и id действия - это не строка, а массив так как:
    • вторым и последующим элементом может быть переданы $_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,]);
        }
    }
    
    Теперь когда пользователь ответит на опрос, его перебросит на домашнюю страницу. Но он может схитрить - снова вернуться на страницу с формой и ответить заново. Ограничим доступ к форме, если пользователь уже отвечал. Сделаем это через всю туже сессию - когда пользователь открывать страницу с формой, в контроллере будет срабатывать проверка по поиску определённого ключа в сессии, если он найден, то запретим пользователю дальнейшую работу с формой. Ключ в сессии будем создавать только, когда форма прошла валидацию.
    Такое ограничение нам может понадобиться в будущем в разных ситуациях: участие в акциях, опросах, вручение подарков и прочее. Поэтому давайте сделаем так, чтобы можно было легко использовать наш код в разных действиях контроллеров. Т.е. если писать:
    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 "ДОСТУП ЗАКРЫТ";
        }
    }
    
    то нужно будет в других действиях проделывать аналогичные действия. В yii\base\Controller есть константы:
    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 используется yii\base\Behavior
    Рекомендуется ознакомится с информацией о поведениях в 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
    {
    
    }
    
    допишем в контроллере:
    'accessOnce' => [
        'class' => \frontend\behaviors\AccessOnce::className(),
    ],
    
    В принципе это "пустышка", т.е. если перейти на страницу с формой "Опроса", то ничего изменится. Но когда поведение прикреплено к наследнику базового класса yii\base\Object, т.е. к почти ко всем классам Yii, то в поведении, после создания его объекта, свойству $owner присваивается объект, который вызвал это поведение. По-простому: объект класса SiteController является владельцем поведения AccessOnce и может быть в поведении получен через $this->owner. Следовательно, становится доступно влиять на события владельца поведения, через $this->owner->on(...). Но опять же, куда вставлять этот код? Логичнее было бы прикрепить обработчик события при создании объекта поведения, делается это через переопределение метода yii\base\Behavior::events():
    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();
        }
    }
    
    Т.к. поведение может быть прикреплено к разным объектам(контроллерам, моделям, представлениями и прочему), то событий EVENT_BEFORE_ACTION и EVENT_AFTER_ACTION у этих объектов может и не быть. Поэтому вводим дополнительную проверку
    if ($owner instanceof Controller) {
    
    которая ограничит неверное использование поведения AccessOnce.
    Теперь создадим обработчиков, которые будут срабатывать при наступлении событий:
    public function имя_обработчика($event)
    {
        
    }
    
    В обработчике будет доступно, $event - наследник класса yii\base\Event Наследник определяется в зависимости от того, кто это событие вызвал. В данном случае $event - yii\base\ActionEvent, т.к. в любом контроллере присутствует код:
    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.
    После всего сделанного, в итоге имеем:
    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();
        } 
        
        //...
    }
    
    Первый раз EVENT_BEFORE_ACTION пускает нас на страницу, так как не обнаруживает переменную в сессии. EVENT_AFTER_ACTION устанавливает эту переменную и при повторном заходе на страницу, EVENT_BEFORE_ACTION нас уже не пропускает. Чтобы изменить такое поведение, нужно отключить EVENT_AFTER_ACTION, если данные не корректные. Для этого отключаем поведение, через Controller::detachBehaviors:
    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());
            }
        }
        $this->detachBehaviors('accessOnce');
        return $this->render('interview', ['model' => $model,]);
    }
    
    Теперь обработчик EVENT_BEFORE_ACTION 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) подразумевает под собой следующий сценарий:
    1. вызывается beforeValidate(), если $runValidation = true. Если $runValidation = false этот и последующий шаги игнорируется.
    2. вызывается afterValidate().
    3. вызывается beforeSave(). Если метод возвращает false, то процесс прерывается и дальнейшие шаги не выполняются.
    4. происходит сохранение данных в базу данных
    5. вызывается 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:
    git checkout -f step-0.4
    
    Входной файл административного раздела (далее Backend) доступен по ссылке /yii2-app-advanced/backend/web/index.php, а все файлы для работы backend располагаются в директории /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 Class common\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:
    new ActiveDataProvider([
        'query' => Interview::find(), 
    ]);
    
    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. Создадутся файлы:
    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
    
    которые и будут являться исполнителями. В последствии, тоже самое нужно проделать и для backend части.

    Запуск тестов

    Можно попробовать для frontend запустить тесты. В yii2-tutorial\yii2-app-advanced\tests\codeception\frontend\ запустим на выполнение все функциональные тесты:
    codecept run functional
    
    Time: 3 seconds, Memory: 29.25Mb
    OK (6 tests, 49 assertions)
    
    Выполнив эту команду, вы наверное ничего и не почувствовали. Но обратите внимание на время выполнения - 3 секунды. За эти три секунды FunctionalTester успел посетить страницу о нас, обратная связь, домашнюю страницу и проверить что они работают без ошибок. В эти 3 секунды исполнитель побывал на странице регистрации, аутентификации, и на странице опроса, проверил эти формы, на корректность ввода данных. Исполнитель проверил работоспособность поведения accessOnce, которое было создано ранее. В сумме за три секунды исполнитель выполнил 6 тестов, в которых присутствовало 49 проверок.
    Представьте сколько бы времени у вас ушло, если бы это выполняли самостоятельно через браузер. И запустив
    codecept run functional
    
    после очередного рефакторинга кода, можно с уверенностью сказать, корректно ли работает сайт. А не бродить по сайту в поисках "А не поломал ли я чего-нибудь?". В этом и есть одна из приятных особенностей тестирования.
    Также можно попробовать запустить модульные тесты:
    codecept run unit
    
    Time: 8.29 seconds, Memory: 17.00Mb
    OK (9 tests, 25 assertions)
    
    А вот для приёмочных, вы обнаружите ошибки.
    codecept run acceptance
    
    FAILURES!
    Tests: 5, Assertions: 0, Errors: 5.
    
    Всё дело в том, что для работы приёмочных тестов нужен браузер. Окружение для Codeception в Yii настроено таким образом, что используется PhpBrowser.
    // Файл yii2-app-advanced/tests/codeception/frontend/acceptance.suite.yml
    modules:
        enabled:
            - PhpBrowser
            - tests\codeception\common\_support\FixtureHelper
        config:
            PhpBrowser:
                url: http://localhost:8080
    
    Т.е. настраивать ничего не нужно, остаётся изменить только путь к нашему сайту с http://localhost:8080 на http://localhost:8888/yii2-app-advanced/. Сделайте это.
    Вместо 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
    | id | name |
    |----|------|
    |    |      |
    
    Planet
    | id | name | star_id |
    |----|------|---------|
    |    |      |         |
    
    Satellite
    | 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:
     'db' => [
        'class' => 'yii\db\Connection',
        'dsn' => 'sqlite:' . __DIR__ . '/../../sqlite.db',
        'tablePrefix' => 'astro',
    ],
    
    Так как наша миграция, может в будущем использоваться не только на SQLite, но и на Mysql, то для Mysql с помощью $tableOptions устанавливаем кодировку и ENGINE=InnoDB, для работы с внешними ключами FOREIGN KEY. В SQLite по умолчанию проверка внешних ключей отключена. Для того, чтобы её включить необходимо выполнить команду:
    PRAGMA foreign_keys = ON;
    
    Выполнять её требуется всякий раз, когда устанавливается соединение с базой данных. У класса, который в нашем случае отвечает за соединение, yii\db\Connection есть события:
    • 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() {..}
    
    Для того, что получить все спутники для планеты Марс, нужно обратиться к коду:
    $marsModel = Planet::find()->where(['name'=>'Марс'])->one();
    $marsModel->getSatellites()->all();
    
    Например, у Юпитера 67 спутников, а нужно получить только 10 первых, которые отсортированы по имени:
    $marsModel = Planet::find()->where(['name'=>'Юпитер'])->one();
    $marsModel->getSatellites()->limit(10)->orderBy(['name'=>SORT_ASC])->limit(10)->all();
    
    Результатом будет массив Active Record моделей. Иногда, для экономии памяти, результат стоит возвращать в виде массива значений с помощью ->asArray()->all().
    Эти примеры выполняли в два этапа: находилась модель, находились отношения, если в этом была необходимость. Можно сделать тоже самое в один запрос:
    $marsModel = Planet::find()->with('satellites')->where(['name'=>'Юпитер'])->one();
    
    Уже упоминалось, что почти каждый класс в Yii наследует 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
    codecept run functional functional\AstroCept.php
    
    Time: 519 ms, Memory: 21.00Mb
    OK (1 test, 3 assertions)
    
    После того, как Gii сгенерировал контроллеры и виды, станут доступны следующие url:
    Эти страницы служат интерфейсом, отправной точкой для работы с моделями. С этих страниц можно попасть на формы для создания или изменения информации по звёздам, планетам и их спутникам. На данный момент база данных не содержит какой-либо информации по звёздам, планетам и их спутникам. Перейдите на следующий шаг, в котором в базу данных добавлена эта информация:
    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.
    class SearchPlanet extends Planet
    {
        public $countSatellites;
    
        /**
         * @inheritdoc
         */
        public function rules()
        {
            return [
                [['id', 'star_id', 'countSatellites'], 'integer'],
                [['name'], 'safe'],
            ];
        }
        
    
    Теперь на странице появилось поле input для фильтрации, но фильтрация и сортировка по этому полю по-прежнему не работает. Осталось настроить $dataProvider, а именно свойство $sort и $query yii\data\ActiveDataProvider. Сортировка и фильтрация выполняется путём добавления к sql запросу ORDER BY или WHERE. Т.е. когда сработает сортировка по полю countSatellites, то должен выполнится запрос
    SELECT * FROM Planet ORDER BY countSatellites ASC|DESC;
    
    Так как в таблице Planet нету поля countSatellites, то такой запрос не выполнится. Поэтому нужно изменить запрос, чтобы в нём участвовал countSatellites. На данный момент в методе search() модели backend\models\SearchPlanet у нас:
    $query = Planet::find(); // эквивалентно выполнению SELECT * FROM Planet
    
    Нам нужно изменить его так, чтобы в запросе участвовало countSatellites:
    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;
    
    Когда есть sql запрос, то не составит труда его переделать в $query (ActiveQuery):
    $query = Planet::find()
            ->select([$this->tableName() . '.*', 'count(planet_id) as countSatellites'])
            ->joinWith('satellites')
            ->groupBy($this->tableName() . '.id');
    
    Теперь сортировка работает для всех полей. Проверьте - управление Planet. Осталось настроить фильтрацию для поля 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 урок

    Сохранение реляционных данных.

    Формы для сохранения данных

    Вы наверное уже обратили внимание на формы для сохранения:
    Сейчас они выглядит, мягко говоря, не удобно для того, чтобы ими пользоваться.
    Давайте начнём с первой формы - сохранение информации по звёздам. Чтобы найти файл формы - смотрим на url star/create, далее открываем контроллер StarController, ищем в нём метод actionCreate(). Видим, что вызывается вид create. Следовательно открываем yii2-app-advanced/backend/views/star/create.php и обнаруживаем, что в этом файле нет нам уже знакомого класса ActiveForm для работы с формами. А есть:
    $this->render('_form', ['model' => $model,])
    
    Как уже известно в видах $this - это yii\web\View. Метод 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:
    <?= $form->field($model, 'name')->textInput(['maxlength' => 255, 'placeholder'=>'Введите название звезды']) ?>
    
    С Yii версии 2.0.3 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(), который вернёт массив моделей.
    $stars = [];
    
    foreach (Star::find()->all() as $star){
        $stars[$star->id] = $star->name;
    }
    
    echo $form->field($model, 'planet')->dropDownList($stars);
    
    Но можно переписать этот код с использованием класса помощника yii\helpers\ArrayHelper, который позволяет обращаться с массивами более эффективно:
    $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');
    
    Можно запустить этот тест:
    codecept run functional functional/PlanetFormCept.php
    
        Time: 1.51 seconds, Memory: 13.25Mb    
        OK (1 test, 0 assertions)
    
    Как видно запустился 1 тест и выполнилось 0 проверок. Теперь к тесту добавим команду на открытие страницы с формой. ля этого нужно создать объект этой страницы. В директории 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 и найдите его метод:
    public function fixtures()
    {
        return [
            'user' => [
                'class' => UserFixture::className(),
                'dataFile' => '@tests/codeception/common/fixtures/data/init_login.php',
            ],
        ];
    }
    
    Запуская каждый раз любой функциональный тест из backend, запускается 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 вернёт утвердительный результат и тест выполнится успешно. Лучше заменить эту проверку на:
    $I->seeInTitle('Новая Земля');
    
    Теперь мы можем смело вносить изменения в код формы и запуская тест видеть, что всё работает корректно. Причём затраты по времени на проверку составит всего около 1 секунды, в то время как раньше, когда вы проверяли форму самостоятельно через браузер, уходило куда больше времени. Для закрепления основ самостоятельно напишите тесты к формам для сохранения звёзд и спутников.

    Сохранение реляционных данных.

    У нас есть три формы для трёх разных моделей. Представьте себе ситуацию: нужно ввести информацию по новой планете, но звезды у неё ещё нету. Не совсем удобно переключаться с формы на форму, сохраняя новые данные. Давайте объединим работу с тремя формами в одной. Для этого нам понадобится новая модель формы, которая будет объединять работу с тремя моделями относительно модели Планет.

    Дополнительная информация для самостоятельного ознакомления:


Комментарии

Популярные сообщения из этого блога

Пишем логи на C# (.NET). Легкий способ.