composer create-project laravel/laravel ... composer require yiisoft/yii2
Что из этого получилось, читайте под катом. Потребовалось написать пару небольших обвязок и сконфигурировать определенным образом, но в целом все работает — и грид, и формы. Также есть небольшой обзор существующих аналогов для Laravel.
Какие есть варианты
https://github.com/view-components/grids
https://github.com/assurrussa/grid-view-table
https://github.com/dwightwatson/bootstrap-form
https://github.com/core-system/bootstrap-form
https://github.com/adamwathan/bootforms
https://github.com/zofe/rapyd-laravel
Основные требования:
— верстка Bootstrap
— автоматическая обработка сортировки, пагинации, ошибок валидации формы
— минимум кода, написанного вручную
— кастомизируемость
https://github.com/view-components/grids
Хороший грид, не зависит от фреймворков. Для фреймворков есть коннекторы. Довольно громоздкая конфигурация. К тому же, похоже, выводимые данные не экранируются.
https://github.com/assurrussa/grid-view-table
Много бойлерплейта, добавляет свою глобальную функцию, какой-то странный способ рендеринга.
https://github.com/dwightwatson/bootstrap-form
Форма сама выбирает роуты для action, ошибки берутся из сессии. Но в целом близко к тому, что нужно.
Мне не нравится подход с передачей ошибок и введенных значений через сессию. Через F5 форму повторно не отправить, если обновить случайно, то все ошибки и значения стираются.
https://github.com/core-system/bootstrap-form
Какой смысл в билдере, если открывать/закрывать группу тегов надо вручную.
https://github.com/core-system/bootstrap-form
Хороший форм-билдер, практически полный аналог ActiveForm. Можно задать хранилище ошибок и введенных значений.
https://github.com/zofe/rapyd-laravel
Этот вариант кажется наиболее подходящим. Есть и грид, и формы. Грид вполне неплохой, но с формами проблема.
— Действия view/create/edit висят на одном роуте, различаются через get-параметр. Соответственно и в гриде по умолчанию URL для действий такие же.
— Это одна форма, просто различается режимом отображения. Это создает проблемы, если надо created_at/updated_at показывать только для view. И свой класс для поля надо описывать для всех 3 режимов.
— Не очень хороший код в проекте
Интеграция
Буду делать по шагам, окончательный код можно найти в конце статьи. Так как таблицы и полное редактирование часто встречаются в административном разделе, пример будет в виде админки для некоторого сайта.
Для справки, папка laravel занимает 2.6 Мб, папка symfony 4.6 Мб, папка yiisoft 3.9 Мб, зависимости Yii 5.6 Мб.
Рассмотрим простое приложение с заказами и товарами.
CREATE TABLE IF NOT EXISTS `users` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `email` varchar(100) NOT NULL, `password` varchar(255) NOT NULL, `remember_token` varchar(100) DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `users_email_unique` (`email`) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS `products` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS `orders` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(10) unsigned NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `orders-users` (`user_id`), CONSTRAINT `orders-users` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS `order_items` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `order_id` int(10) unsigned NOT NULL, `product_id` int(10) unsigned NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `order_items-orders` (`order_id`), KEY `order_items-products` (`product_id`), CONSTRAINT `order_items-orders` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `order_items-products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON UPDATE CASCADE ) ENGINE=InnoDB;
Создадим Eloquent модели и OrderController для раздела заказов. Создадим группу роутов для админки.
routes/web.phpRoute::group(['prefix' => 'admin', 'as' => 'admin.', 'namespace' => 'Admin'], function () { Route::get('/order', 'OrderController@index')->name('order.index'); Route::get('/order/view/{id}', 'OrderController@view')->name('order.view'); Route::get('/order/create', 'OrderController@create')->name('order.create'); Route::get('/order/update/{id}', 'OrderController@update')->name('order.update'); Route::post('/order/create', 'OrderController@create'); Route::post('/order/update/{id}', 'OrderController@update'); Route::post('/order/delete/{id}', 'OrderController@delete')->name('order.delete'); });
Создадим Bootstrap-шаблон со ссылками на CDN.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" href="/favicon.ico"> <title>@yield('title')</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <![endif]--> <style>body { padding-top: 60px; }</style> </head> <body> @include('layouts.nav') <div class="container"> @yield('content') </div> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> </body> </html>
Делаем middleware с инициализацией и подключаем к роутам админки. Инициализация выглядит так.
routes/web.php$initYii2Middleware = function ($request, $next) { define('YII_DEBUG', env('APP_DEBUG')); include '../vendor/yiisoft/yii2/Yii.php'; spl_autoload_unregister(['Yii', 'autoload']); $config = [ 'id' => 'yii2-laravel', 'basePath' => '../', 'timezone' => 'UTC', 'components' => [ 'assetManager' => [ 'basePath' => '@webroot/yii-assets', 'baseUrl' => '@web/yii-assets', 'bundles' => [ 'yii\web\JqueryAsset' => [ 'sourcePath' => null, 'basePath' => null, 'baseUrl' => null, 'js' => [], ], ], ], 'request' => [ 'class' => \App\Yii\Web\Request::class, 'csrfParam' => '_token', ], 'urlManager' => [ 'enablePrettyUrl' => true, 'showScriptName' => false, ], 'formatter' => [ 'dateFormat' => 'php:m/d/Y', 'datetimeFormat' => 'php:m/d/Y H:i:s', 'timeFormat' => 'php:H:i:s', 'defaultTimeZone' => 'UTC', ], ], ]; (new \yii\web\Application($config)); // initialization is in constructor Yii::setAlias('@bower', Yii::getAlias('@vendor') . DIRECTORY_SEPARATOR . 'bower-asset'); return $next($request); }; Route::group(['prefix' => 'admin', 'as' => 'admin.', 'namespace' => 'Admin', 'middleware' => $initYii2Middleware], function () { ... });
spl_autoload_unregister(['Yii', 'autoload']); — лучше отключить, чтобы не мешался, достаточно автозагрузчиков Laravel. Он ищет файлы через getAlias('@'...) и конечно не находит.basePath — корневая директория приложения, при неправильной установке могут быть ошибки в путях. В этой же директории создается папка runtime.assetManager.basePath, assetManager.baseUrl — путь и URL для публикации ассетов, название папки произвольное.assetManager.bundles — отключаем публикацию jQuery, так как она подключается в главном шаблоне отдельно.request — переопределяем компонент запроса, в котором заменяем работу с CSRF-токеном, название поля такое же как в настройках Laravel.urlManager.enablePrettyUrl — надо включить, если нужны дополнительные модули типа Gii.(new \yii\web\Application($config)) — в конструкторе происходит присвоение Yii::$app = $this;Компонент запроса выглядит так:
app/Yii/Web/Request.phpnamespace App\Yii\Web; class Request extends \yii\web\Request { public function getCsrfToken($regenerate = false) { return \Session::token(); } }
Токеном управляет Laravel, поэтому регенерацию обрабатывать не надо.
Грид
Теперь можно попробовать запустить. Добавим код для списка заказов.
app/Http/Controllers/Admin/OrderController.phppublic function index(Request $request) { $allModels = Order::query()->get()->all(); $gridViewConfig = [ 'dataProvider' => new \yii\data\ArrayDataProvider([ 'allModels' => $allModels, 'pagination' => ['route' => $request->route()->uri(), 'defaultPageSize' => 10], 'sort' => ['route' => $request->route()->uri(), 'attributes' => ['id']], ]), 'columns' => [ 'id', 'user.name', ['label' => 'Items', 'format' => 'raw', 'value' => function ($model) { $html = ''; foreach ($model->items as $item) { $html .= '<div>' . htmlspecialchars($item->product->name) . '</div>'; } return $html; }], 'created_at:datetime', 'updated_at:datetime', [ 'class' => \yii\grid\ActionColumn::class, 'urlCreator' => function ($action, $model, $key) use ($request) { $baseRoute = $request->route()->getName(); $baseRouteParts = explode('.', $baseRoute); $baseRouteParts[count($baseRouteParts) - 1] = $action; $route = implode('.', $baseRouteParts); $params = is_array($key) ? $key : ['id' => (string) $key]; return route($route, $params, false); } ], ], ]; return view('admin.order.index', ['gridViewConfig' => $gridViewConfig]); }
@extends('layouts.main') @section('title', 'Index') @section('content') <h1>Orders</h1> <div class="text-right"> <a href="{{ route('admin.order.create') }}" class="btn btn-success">Create</a> </div> {!! \yii\grid\GridView::widget($gridViewConfig) !!} @endsection
Нужно установить
dataProvider.pagination.route и dataProvider.sort.route, иначе произойдет обращение к Yii::$app->controller->getRoute(), а контроллер у нас null. Аналогично с ActionColumn, только там будет проверка и InvalidParamException. URL генерируется через \yii\web\UrlManager, но результат получается такой же, как с роутингом Laravel. Можно задать менеджер через dataProvider.pagination.urlManager, если нужно.Метки колонок пока оставим автогенерируемые.
Также надо задать некоторые стили для иконок сортировки.
Грид выводится, но так как нет фронтенд-скриптов, то кнопка Delete не работает.
Надо вывести скрипты, которые находятся в компоненте
\yii\web\View. Методы renderHeadHtml(), renderBodyBeginHtml(), renderBodyEndHtml() защищены (непонятно от кого, особенно учитывая, что все переменные public). Как ни странно, есть повод применить антипаттерн «public morozov». Или можно просто скопипастить их в главный шаблон.app/Yii/Web/View.phpnamespace App\Yii\Web; class View extends \yii\web\View { public function getHeadHtml() { return parent::renderHeadHtml(); } public function getBodyBeginHtml() { return parent::renderBodyBeginHtml(); } public function getBodyEndHtml($ajaxMode = false) { return parent::renderBodyEndHtml($ajaxMode); } public function initAssets() { \yii\web\YiiAsset::register($this); ob_start(); $this->beginBody(); $this->endBody(); ob_get_clean(); } }
В Yii регистрация ассетов происходит в функции
endBody(), а также весь рендеринг оборачивается в буфер, в котором потом производится замена магических констант CDATA на реальные ассеты. Эмуляция этого поведения находится в функции initAssets(). Заменять мы ничего не будем, нам нужно просто чтобы были заполнены свойства $this->js, $this->css и другие.'components' => [ ... 'view' => [ 'class' => \App\Yii\Web\View::class, ], ],
<!DOCTYPE html> <html lang="en"> <head> ... <?php $view = \Yii::$app->getView(); $view->initAssets(); ?> {!! \yii\helpers\Html::csrfMetaTags() !!} {!! $view->getHeadHtml() !!} </head> <body> {!! $view->getBodyBeginHtml() !!} ... {!! $view->getBodyEndHtml() !!} </body> </html>
Вызов
Html::csrfMetaTags() нужен, так как скрипт yii.js берет csrf-токен из HTML страницы.ArrayDataProvider работает, но надо сделать аналог ActiveDataProvider, чтобы получать из базы только то что нужно.app/Yii/Data/EloquentDataProvider.phpclass EloquentDataProvider extends \yii\data\BaseDataProvider { public $query; public $key; protected function prepareModels() { $query = clone $this->query; if (($pagination = $this->getPagination()) !== false) { $pagination->totalCount = $this->getTotalCount(); if ($pagination->totalCount === 0) { return []; } $query->limit($pagination->getLimit())->offset($pagination->getOffset()); } if (($sort = $this->getSort()) !== false) { $this->addOrderBy($query, $sort->getOrders()); } return $query->get()->all(); } protected function prepareKeys($models) { $keys = []; if ($this->key !== null) { foreach ($models as $model) { $keys[] = $model[$this->key]; } return $keys; } else { $pks = $this->query->getModel()->getKeyName(); if (is_string($pks)) { $pk = $pks; foreach ($models as $model) { $keys[] = $model[$pk]; } } else { foreach ($models as $model) { $kk = []; foreach ($pks as $pk) { $kk[$pk] = $model[$pk]; } $keys[] = $kk; } } return $keys; } } protected function prepareTotalCount() { $query = clone $this->query; $query->orders = null; $query->offset = null; return (int) $query->limit(-1)->count('*'); } protected function addOrderBy($query, $orders) { foreach ($orders as $attribute => $order) { if ($order === SORT_ASC) { $query->orderBy($attribute, 'asc'); } else { $query->orderBy($attribute, 'desc'); } } } }
'dataProvider' => new \App\Yii\Data\EloquentDataProvider([ 'query' => Order::query(), 'pagination' => ['route' => $request->route()->uri(), 'defaultPageSize' => 10], 'sort' => ['route' => $request->route()->uri(), 'attributes' => ['id']], ]),
Метки и фильтры
Нужно сделать базовую модель, унаследованную от
\yii\base\Model, которая будет возвращать гриду метки для колонок и правила полей для фильтрации. Для этого есть параметр filterModel. Сделаем ее конфигурируемой через конструктор.namespace App\Yii\Data; use App\Yii\Data\EloquentDataProvider; use Route; class FilterModel extends \yii\base\Model { protected $labels; protected $rules; protected $attributes; public function __construct($labels = [], $rules = []) { parent::__construct(); $this->labels = $labels; $this->rules = $rules; $safeAttributes = $this->safeAttributes(); $this->attributes = array_combine($safeAttributes, array_fill(0, count($safeAttributes), null)); } public function __get($name) { if (array_key_exists($name, $this->attributes)) { return $this->attributes[$name]; } else { return parent::__get($name); } } public function __set($name, $value) { if (array_key_exists($name, $this->attributes)) { $this->attributes[$name] = $value; } else { parent::__set($name, $value); } } public function rules() { return $this->rules; } public function attributeLabels() { return $this->labels; } public function initDataProvider($query, $sortAttirbutes = [], $route = null) { if ($route === null) { $route = Route::getCurrentRoute()->uri(); } $dataProvider = new EloquentDataProvider([ 'query' => $query, 'pagination' => ['route' => $route], 'sort' => ['route' => $route, 'attributes' => $sortAttirbutes], ]); return $dataProvider; } public function applyFilter($params) { $query = null; $dataProvider = $this->initDataProvider($query); return $dataProvider; } }
Можно унаследоваться и определить специализированную модель, и поместить все туда.
namespace App\Forms\Admin; use App\Yii\Data\FilterModel; class OrderFilter extends FilterModel { public function rules() { return [ ['id', 'safe'], ['user.name', 'safe'], ]; } public function attributeLabels() { return [ 'id' => 'ID', 'created_at' => 'Created At', 'updated_at' => 'Updated At', 'user.name' => 'User', ]; } public function applyFilter($params) { $this->load($params); $query = \App\Models\Order::query(); $query->join('users', 'users.id', '=', 'orders.user_id')->select('orders.*'); if ($this->id) $query->where('orders.id', '=', $this->id); if ($this->{'user.name'}) $query->where('users.name', 'like', '%'.$this->{'user.name'}.'%'); $sortAttributes = [ 'id', 'user.name' => ['asc' => ['users.name' => SORT_ASC], 'desc' => ['users.name' => SORT_DESC]], ]; $dataProvider = $this->initDataProvider($query, $sortAttributes); $dataProvider->pagination->defaultPageSize = 10; if (empty($dataProvider->sort->getAttributeOrders())) { $dataProvider->query->orderBy('orders.id', 'asc'); } return $dataProvider; } }
public function index(Request $request) { $filterModel = new \App\Forms\Admin\OrderFilter(); $dataProvider = $filterModel->applyFilter($request); $gridViewConfig = [ 'dataProvider' => $dataProvider, 'filterModel' => $filterModel, ... ]; ... }
Есть небольшая проблема, что если задана filterModel, но нет ни одного поля для фильтра, то все равно отображается строка с пустыми ячейками. В этом случае можно метки вручную проставить. Хотя лучше было бы, если бы в самом компоненте была такая проверка.
Просмотр
Тут делаем аналогично, настройки колонок можно скопировать из грида. Товары в заказе сделаем отдельным гридом на странице просмотра. Метки тоже пока оставим автогенерируемые.
app/Http/Controllers/Admin/OrderController.phppublic function view($id) { $model = Order::findOrFail($id); $detailViewConfig = [ 'model' => $model, 'attributes' => [ 'id', 'user.name', 'created_at:datetime', 'updated_at:datetime', ], ]; $gridViewConfig = [ 'dataProvider' => new \App\Yii\Data\EloquentDataProvider([ 'query' => $model->items(), 'pagination' => false, 'sort' => false, ]), 'layout' => '{items}{summary}', 'columns' => [ 'id', 'product.name', 'created_at:datetime', 'updated_at:datetime', ], ]; return view('admin.order.view', ['model' => $model, 'detailViewConfig' => $detailViewConfig, 'gridViewConfig' => $gridViewConfig]); }
@extends('layouts.main') @section('title', 'Index') @section('content') <h1>Order: {{ $model->id }}</h1> <p class="text-right"> <a href="{{ route('admin.order.update', ['id' => $model->id]) }}" class="btn btn-primary">Update</a> <a href="{{ route('admin.order.delete', ['id' => $model->id]) }}" class="btn btn-danger" data-confirm="Are you sure?" data-method="post">Delete</a> </p> {!! \yii\widgets\DetailView::widget($detailViewConfig) !!} <h2>Order Items</h2> {!! \yii\grid\GridView::widget($gridViewConfig) !!} @endsection
Создание / Обновление
Сначала нужно сделать модель формы, враппер для Eloquent моделей, унаследованный от
\yii\base\Model, чтобы компонент ActiveForm мог вызывать нужные методы.namespace App\Yii\Data; use Illuminate\Database\Eloquent\Model as EloquentModel; class FormModel extends \yii\base\Model { protected $model; protected $labels; protected $rules; protected $attributes; public function __construct(EloquentModel $model, $labels = [], $rules = []) { parent::__construct(); $this->model = $model; $this->labels = $labels; $this->rules = $rules; $fillable = $model->getFillable(); $attributes = []; foreach ($fillable as $field) { $attributes[$field] = $model->$field; } $this->attributes = $attributes; } public function getModel() { return $model; } public function __get($name) { if (array_key_exists($name, $this->attributes)) { return $this->attributes[$name]; } else { return $this->model->{$name}; } } public function __set($name, $value) { if (array_key_exists($name, $this->attributes)) { $this->attributes[$name] = $value; } else { $this->model->{$name} = $value; } } public function rules() { return $this->rules; } public function attributeLabels() { return $this->labels; } public function save() { if (!$this->validate()) { return false; } $this->model->fill($this->attributes); return $this->model->save(); } }
Теперь можно сделать редактирование.
app/Http/Controllers/Admin/OrderController.phppublic function create(Request $request) { $model = new Order(); $formModel = new \App\Yii\Data\FormModel( $model, ['user_id' => 'User'], [['user_id', 'safe']] ); if ($request->isMethod('post')) { if ($formModel->load($request->input()) && $formModel->save()) { return redirect()->route('admin.order.view', ['id' => $model->id]); } } return view('admin.order.create', ['formModel' => $formModel]); } public function update($id, Request $request) { $model = Order::findOrFail($id); $formModel = new \App\Yii\Data\FormModel( $model, ['user_id' => 'User'], [['user_id', 'safe']] ); if ($request->isMethod('post')) { if ($formModel->load($request->input()) && $formModel->save()) { return redirect()->route('admin.order.view', ['id' => $model->id]); } } return view('admin.order.update', ['formModel' => $formModel]); }
<?php $form = \yii\widgets\ActiveForm::begin() ?> {!! $form->field($formModel, 'user_id')->dropDownList(\App\User::pluck('name', 'id'), ['prompt' => '']) !!} <button type="submit" class="btn btn-primary">Submit</button> <?php \yii\widgets\ActiveForm::end() ?>
Правила валидации задаются в стиле Yii. Если нужно, можно переопределить метод
validate() и вызывать там валидатор Laravel. В данном примере мы этого делать не будем.Blade не разрешает объявлять переменные. А
ActiveForm::begin() и выводит теги и возвращает значение. Можно явно написать тег <?php ?>, можно сделать новый тег через Blade::extend(), как советуют здесь, можно сделать обертку для ActiveForm. Пока оставим <?php ?>.Как и в случае с фильтром, можно унаследоваться от
FormModel и поместить все объявления туда.namespace App\Forms\Admin; class OrderForm extends FormModel { public function rules() { return [ ['user_id', 'safe'], ]; } public function attributeLabels() { return [ 'id' => 'ID', 'user_id' => 'User', 'created_at' => 'Created At', 'updated_at' => 'Updated At', 'user.name' => 'User', ]; } }
Метки на странице просмотра
Теперь можно использовать
OrderForm, чтобы задать метки в методе app/Http/Controllers/Admin/OrderController.php.$formModel = new \App\Forms\Admin\OrderForm($model); $detailViewConfig = [ 'model' => $formModel, ... ];
Удаление
Тут все просто.
app/Http/Controllers/Admin/OrderController.phppublic function delete($id) { $model = Order::findOrFail($id); $model->delete(); return redirect()->route('admin.order.index'); }
Дополнения
Можно подключить Gii. В чистом виде он не нужен, но можно брать из генератора моделей правила валидации формы и метки для полей, чтобы не генерировать их вручную. Или можно свой генератор написать.
composer require yiisoft/yii2-gii --dev
$config = [ 'components' => [ ... 'db' => [ 'class' => \yii\db\Connection::class, 'dsn' => 'mysql:host='.env('DB_HOST', 'localhost') .';port='.env('DB_PORT', '3306') .';dbname='.env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'charset' => 'utf8', ], ... ], ]; if (YII_DEBUG) { $config['modules']['gii'] = ['class' => \yii\gii\Module::class]; $config['bootstrap'][] = 'gii'; } (new \yii\web\Application($config)); // initialization is in constructor Yii::setAlias('@bower', Yii::getAlias('@vendor') . DIRECTORY_SEPARATOR . 'bower-asset'); Yii::setAlias('@App', Yii::getAlias('@app') . DIRECTORY_SEPARATOR . 'App'); ... Route::any('gii{params?}', function () { $request = \Yii::$app->getRequest(); $request->setBaseUrl('/admin'); \Yii::$app->run(); return null; })->where('params', '(.*)');
Yii::setAlias('@App') — путь к файлам определяется через Yii::getAlias('@'...), поэтому для класса App\Models\Order будет проверяться путь '@App/Models/Order.php'.setBaseUrl('/admin') — нужно, чтобы роутинг Yii обрабатывал только часть после '/admin'.С
Yii::setAlias('@App') и ['Yii', 'autoload'] есть такая проблема. Если не отключить автозагрузчик, то при неправильном названиии класса или неймспейса в существующем файле происходит ошибка, которая неправильно обрабатывается. Происходит это так. Он подключает файл, но потом не находит класс и бросает исключение UnknownClassException. Вызывается автозагрузчик Laravel, который проверяет фасады и алиасы и тоже ничего не находит. Потом вызывается автозагрузчик Composer, который снова подключает файл, и возникает уже другая ошибка 'Cannot declare class '...', because the name is already in use'. Приложение падает с ошибкой 500 без записи в лог.Gii будет работать, несмотря на то, что мы отключили jQuery, так как у него свой шаблон отображения, и поэтому он сбрасывает настройки ассетов приложения.
protected function resetGlobalSettings() { if (Yii::$app instanceof \yii\web\Application) { Yii::$app->assetManager->bundles = []; } }
Можно вынести конфигурацию
ActionColumn в отдельный класс, чтобы не копировать в разные гриды.namespace App\Yii\Widgets; use URL; use Route; class ActionColumn extends \yii\grid\ActionColumn { public $keyAttribute = 'id'; public $baseRoute = null; public $separator = '.'; /** * Overrides URL generation to use Laravel routing system * * @inheritdoc */ public function createUrl($action, $model, $key, $index) { if (is_callable($this->urlCreator)) { return call_user_func($this->urlCreator, $action, $model, $key, $index, $this); } else { if ($this->baseRoute === null) { $this->baseRoute = Route::getCurrentRoute()->getName(); } $baseRouteParts = explode($this->separator, $this->baseRoute); $baseRouteParts[count($baseRouteParts) - 1] = $action; $route = implode($this->separator, $baseRouteParts); $params = is_array($key) ? $key : [$this->keyAttribute => (string) $key]; return URL::route($route, $params, false); } } }
Можно сделать обертку для ActiveForm, куда поместить вызов виждета, и передавать модель в конструктор. Это позволит убрать прямые теги
<?php ?> и передачу модели в каждое поле. Также туда можно добавлять дополнительные методы для инициализации сторонних виджетов полей типа Select2. Такой билдер можно использовать и в проектах на Yii.namespace App\Yii\Widgets; use yii\widgets\ActiveForm; use yii\helpers\Html; class FormBuilder extends \yii\base\Component { protected $model; protected $form; public function __construct($model) { $this->model = $model; } public function getModel() { return $this->model; } public function setModel($model) { $this->model = $model; } public function getForm() { return $this->form; } public function open($params = ['successCssClass' => '']) { $this->form = ActiveForm::begin($params); } public function close() { ActiveForm::end(); } public function field($attribute, $options = []) { return $this->form->field($this->model, $attribute, $options); } public function submitButton($content, $options = ['class' => 'btn btn-primary']) { return Html::submitButton($content, $options); } }
{!! $form->open() !!} {!! $form->field('user_id')->dropDownList( \App\User::pluck('name', 'id'), ['prompt' => '']) !!} {!! $form->submitButton('Submit'); !!} {!! $form->close() !!}
Исходный код
Исходный код можно найти здесь. Все шаги сделаны отдельными коммитами. Есть миграции и тестовые данные.
php artisan migrate:refresh --seed
Обертки находятся в папке
app/Yii.Обязательные:
App\Yii\Web\Request App\Yii\Data\EloquentDataProvider App\Yii\Data\FormModel
Без остальных можно обойтись, но с ними удобнее:
App\Yii\Data\FilterModel App\Yii\Web\View App\Yii\Widgets\ActionColumn App\Yii\Widgets\FormBuilder
Также, думаю, это неплохой пример для сравнения разных реализаций. Если есть время и желание, приводите в комментариях свою реализацию этой админки на другом стеке технологий.

