Продолжаем разработку нашего чудо-сервиса на Yii2. На прошлом уроке мы создали через Composer новый проект и дополнили его раздельной системой конфигурационных файлов. Сегодня мы внедрим в проект модульную структуру и немного лучше познакомимся с базовыми настройками и некоторыми возможностями авторефакторинга в PhpStorm IDE. Другими IDE я не пользуюсь, поэтому с ними не могу вам помочь.

Предварительная настройка IDE

При работе с использованием IDE нужно учитывать и её нюансы.

Во-первых, у нас в проекте есть временные мусорные папки runtime и web/assets. Их желательно исключить из зоны видимости любой IDE, чтобы они попусту не захламляли её внутреннюю историю изменений, не участвовали в поиске, и чтобы список файлов внутри них не занимал оперативную память.

Во-вторых, у нас есть внушительных размеров папка vendor. Включение её в проект полезно, так как это позволит работать автоподстановке, сканирующей все константы, классы и методы.

Но в IDE кроме сканера автоподстановки имеется множество других инструментов для обхода файлов проекта. Это может быть банальный поиск по содержимому, рефакторинг, поиск ошибок с помощью инспектора Code > Inspect Code или вывод списка TODO из всех файлов. Этим инструментам нежелательно сканировать папку vendor, чтобы, например, не выводить сотни найденных TODO и стилистических ошибок из тех же встроенных минимизированных сркриптов.

Соответственно, в PhpStorm IDE войдем в настройки нашего проекта File > Settings, выделим каждую «лишнюю» папку и исключим её, пометив как Excluded:

phpstorm-exclude

Мы исключили и папку vendor из проекта, поэтому все классы фреймворка потеряются. Нам нужно вернуть её, но при этом не добавлять как папку проекта, а подключить как одну из системных библиотек. Переходим ниже в раздел PHP и добавляем путь в список Include path:

phpstorm-lib

Аналогично сюда можно добавить путь к папке PEAR или PHPUnit, если вы собираетесь их использовать.

Теперь сканер автоподстановки благополучно найдёт все файлы фреймворка, классы будут также находиться через Ctrl+N, но поиск по содержимому будет проводиться только по вашим файлам. Если же нужно будет произвести поиск внутри папки vendor, то просто специально станьте на неё мышкой и нажмите Ctrl+Shift+F для поиска конкретно в ней.

Теперь не будет проблем с заменой какой-либо строки посредством Ctrl+Shift+R, так как автозамена больше не затронет файлы фреймворка.

Продолжим работу с нашим проектом.

Переход к модульной структуре

Хранить все контроллеры и модели в одних и тех же папках может быть удобно, но приятнее разделять функционал на модули.

Сейчас у нас имеется всего один контроллер и три модели:

assets/
commands/
config/
controllers/
    SiteController.php
mail/
models/
    ContactForm.php
    LoginForm.php
    User.php
tests/
vendor/
views/
    layouts/
        main.php
    site/
        about.php
        contact.php
        error.php
        index.php
        login.php
web/

Один контроллер и выводит страницы, и авторизует пользователя, и обеспечивает обратную связь. Исходя из принципа единой ответственности (обязанности) такое смешение не очень оправдано и удобно.

Первым делом откроем web/index.php и проверяем, что там установлено окружение dev:

defined('YII_ENV') or define('YII_ENV', 'dev');

Вспомним, что в config/web.php у нас находится фрагмент:

if (YII_ENV_DEV) {
    // configuration adjustments for 'dev' environment
    $config['bootstrap'][] = 'debug';
    $config['modules']['debug'] = 'yii\debug\Module';
 
    $config['bootstrap'][] = 'gii';
    $config['modules']['gii'] = 'yii\gii\Module';
}

Он определяет, что в окружении разработки (development) к приложению подключаются модулиdebug (выводящий панель отладки и её внутренние страницы) и gii (генератор моделей, модулей и прочего), Вот именно Gii нам и пригодится.

В корневой папке проекта создаём директорию modules. Теперь открываем генератор по адресуhttp://localhost/gii, переходим в «Module Generator» и вводим данные для нового модуля:

gii-module

После нажатия Preview и Generate мы получим соответствующую папку:

modules/
    main/
        controllers/
            DefaultController.php
        views/
            default/
                index.php
        Module.php

Также генератор нам подскажет, что в конфигурационный файл (в нашем случае вconfig/common.php) нужно подключить наш сгенерированный класс:

return [
    ...
    'modules' => [
        'main' => [
            'class' => 'app\modules\main\Module',
        ],
    ],
    ...
];

Аналогично нам нужно сгенерировать и подключить заготовку модуля user. В итоге у нас будет добавлена секция modules с двумя модулями:

return [
    ...
    'bootstrap' => ['log'],
    'modules' => [
        'main' => [
            'class' => 'app\modules\main\Module',
        ],
        'user' => [
            'class' => 'app\modules\user\Module',
        ],
    ],
    'components' => [
        ...
    ],
    'params' => $params,
];

Gii помог нам сгенерировать заготовки. А теперь аккуратная ручная работа. Нужно раскидать действия из SiteController.php, существующие модели и представления по этим модулям.

Сначала переносим модели. Создаём папки modules/main/models и modules/user/models. Если у вас тоже есть хорошая IDE, то самое время задействовать её возможности по автоматическому рефакторингу.

Открываем модель LoginForm.php и щелчком по имени класса правой кнопкой мыши (или горячей клавишей ) выбираем Refactor > Move. В появившемся окне изменяем пространство имён наapp\modules\user\models:

refactor-move

 

и жмём Refactor и после сканирования Do Refactor. Автоматически файл перекинется в новую папку, у класса изменится namespace и во всех местах использования этой модели автоматически поменяется строка use.

Если ваш редактор не обладает таким функционалом, то нужно будет переместить классы вручную.

Аналогичным образом переносим модели User в модуль user и ContactForm в модуль main.

Теперь дело за контроллером SiteController. Его содержимое необходимо разнести по контроллерам всех модулей.

Контроллер обратной связи сделаем отдельным в модуле main:

namespace app\modules\main\controllers;
 
use app\modules\main\models\ContactForm;
use yii\web\Controller;
use Yii;
 
class ContactController extends Controller
{
    public function actions()
    {
        return [
            'captcha' => [
                'class' => 'yii\captcha\CaptchaAction',
                'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
            ],
        ];
    }
 
    public function actionIndex()
    {
        $model = new ContactForm();
        if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) {
            Yii::$app->session->setFlash('contactFormSubmitted');
 
            return $this->refresh();
        } else {
            return $this->render('index', [
                'model' => $model,
            ]);
        }
    }
}

Мы перенесли в него действия captcha и actionContact.

Обратите внимание, что actionContact мы переименовали в actionIndex и соответственно заменили имя файла представления. Теперь файл views/site/contact.php поместим какmodules/main/views/contact/index.php.

Если теперь удалить SiteController и перейти по адресу http://localhost/main/contact/index, то возникнет проблема с кодом подтверждения (captcha) контактной формы. Это происходит из-за того, что адрес по умолчанию /site/captcha теперь недоступен. Нужно указать новый адрес.

В модели ContactForm находим строку подключения CaptchaValidator:

['verifyCode', 'captcha'],

и указываем новый маршрут:

['verifyCode', 'captcha', 'captchaAction' => '/main/contact/captcha'],

Аналогично в файле представления modules/main/views/contact/index.php находим конструкцию вывода виджета:

<?= $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>',
]) ?>

и добавляем такой же параметр:

<?= $form->field($model, 'verifyCode')->widget(Captcha::className(), [
    'captchaAction' => '/main/contact/captcha',
    'template' => '<div class="row"><div class="col-lg-3">{image}</div><div class="col-lg-6">{input}</div></div>',
]) ?>

Теперь контактная форма должна нормально работать.

Контроллер по умолчанию в модуле main будет выводить страницы и ошибки:

namespace app\modules\main\controllers;
 
use yii\web\Controller;
 
class DefaultController extends Controller
{
    public function actions()
    {
        return [
            'error' => [
                'class' => 'yii\web\ErrorAction',
            ],
        ];
    }
 
    public function actionIndex()
    {
        return $this->render('index');
    }
}

К нему нужно перенести все его представления index.php и error.php из папки views/site в папку modules/main/default.

Мы пропустили только действие actionAbout и представление about.php, так как статические страницы нам не нужны. Если потребуется организовать раздел помощи, то его можно сделать потом.

И последний модуль user. В его контроллер мы перенесём авторизацию и представлениеlogin.php:

namespace app\modules\user\controllers;
 
use app\modules\user\models\LoginForm;
use yii\filters\AccessControl;
use yii\filters\VerbFilter;
use yii\web\Controller;
use Yii;
 
class DefaultController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'only' => ['logout'],
                'rules' => [
                    [
                        'actions' => ['logout'],
                        'allow' => true,
                        'roles' => ['@'],
                    ],
                ],
            ],
            'verbs' => [
                'class' => VerbFilter::className(),
                'actions' => [
                    'logout' => ['post'],
                ],
            ],
        ];
    }
 
    public function actionLogin()
    {
        if (!Yii::$app->user->isGuest) {
            return $this->goHome();
        }
 
        $model = new LoginForm();
        if ($model->load(Yii::$app->request->post()) && $model->login()) {
            return $this->goBack();
        } else {
            return $this->render('login', [
                'model' => $model,
            ]);
        }
    }
 
    public function actionLogout()
    {
        Yii::$app->user->logout();
 
        return $this->goHome();
    }
}

Теперь пустые папки controllers, models и views/site в корне можно удалить. Вместо них будет только папка modules.

В итоге получилась новая файловая структура, состоящая из двух модулей:

assets/
commands/
config/
mail/
modules/
    main/
        controllers/
            DefaultController.php
            ContactController.php
        models/
            ContactForm.php
        views/
            default/
                error.php
                index.php
            contact/
                index.php
        Module.php
    user/
        controllers/
            DefaultController.php
        models/
            LoginForm.php
            User.php
        views/
            default/
                login.php
        Module.php
tests/
vendor/
views/
    layouts/
        main.php
web/

И немного доработаем представления. Если зайдём в любое представление вродеmodules/user/views/default/login.php, то увидим там вспомогательный класс, помогающий по HTML-коду страницы в браузере определить текущий маршрут:

<div class="site-login">

Это полезная подсказка для разработчика проекта. Также этот класс может быть полезен для установки персональных стилей для каждой страницы в CSS-файле. Но теперь у нас есть модули и мы перенесли это представление. Оставлять старые значения мы не будем, поэтому теперь пробежимся по всем файлам и поменяем классы на новые. Например, у этого представления теперь новый маршрут:

<div class="user-default-login">

Теперь необходимо немного доработать файлы конфигурации. Первым делом, укажем маршрут по умолчанию вместо site/index и действие для вывода ошибок вместо site/error в config/web.php. Путь к классу User уже должен быть исправлен после автопереноса модели в IDE. Помимо этого нужно добавить новый путь для свойства loginUrl:

$config = [
    'id' => 'app',
    'defaultRoute' => 'main/default/index',
    'components' => [
        'user' => [
            'identityClass' => 'app\modules\user\models\User',
            'enableAutoLogin' => true,
            'loginUrl' => ['user/default/login'],
        ],
        'errorHandler' => [
            'errorAction' => 'main/default/error',
        ],
        'request' => [
            'cookieValidationKey' => '',
        ],
        'log' => [
            'traceLevel' => YII_DEBUG ? 3 : 0,
        ],
    ],
];

Также дополним правила маршрутизации с учётом новых модулей в config/common.php:

return [
    'basePath' => dirname(__DIR__),
    'bootstrap' => ['log'],
    'modules' => [
        'main' => [
            'class' => 'app\modules\main\Module',
        ],
        'user' => [
            'class' => 'app\modules\user\Module',
        ],
    ],
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'charset' => 'utf8',
        ],
        'urlManager' => [
            'class' => 'yii\web\UrlManager',
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules' => [
                '' => 'main/default/index',
                'contact' => 'main/contact/index',
                '<_a:error>' => 'main/default/<_a>',
                '<_a:(login|logout)>' => 'user/default/<_a>',
 
                '<_m:[\w\-]+>/<_c:[\w\-]+>/<_a:[\w\-]+>/<id:\d+>' => '<_m>/<_c>/<_a>',
                '<_m:[\w\-]+>/<_c:[\w\-]+>/<id:\d+>' => '<_m>/<_c>/view',
                '<_m:[\w\-]+>' => '<_m>/default/index',
                '<_m:[\w\-]+>/<_c:[\w\-]+>' => '<_m>/<_c>/index',
            ],
        ],
        'mailer' => [
            'class' => 'yii\swiftmailer\Mailer',
        ],
        'cache' => [
            'class' => 'yii\caching\DummyCache',
        ],
        'log' => [
            'class' => 'yii\log\Dispatcher',
        ],
    ],
    'params' => $params,
];

Теперь исправим маршруты ссылок в главном меню views/layouts/main:

echo Nav::widget([
    'options' => ['class' => 'navbar-nav navbar-right'],
    'items' => [
        ['label' => 'Home', 'url' => ['/main/default/index']],
        ['label' => 'Contact', 'url' => ['/main/contact/index']],
        Yii::$app->user->isGuest ?
            ['label' => 'Login', 'url' => ['/user/default/login']] :
            ['label' => 'Logout (' . Yii::$app->user->identity->username . ')',
                'url' => ['/user/default/logout'],
                'linkOptions' => ['data-method' => 'post']],
    ],
]);

Если вы уже собираетесь использовать автотесты, то исправьте аналогично маршруты в классах в папке tests/_pages и пространства имён форм в tests/unit/models.

Честно украдено в качестве заметки, на всякий пожарный, отсюда.

Комментарии

comments powered by Disqus