Недавно была статья про Yii, где в комментариях обсуждали специфичные для Yii компоненты, в частности GridView и ActiveForm, и фреймворк Laravel. Я подумал, а почему бы и нет.
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.php
Route::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' => [
'yiiwebJqueryAsset' => [
'sourcePath' => null,
'basePath' => null,
'baseUrl' => null,
'js' => [],
],
],
],
'request' => [
'class' => AppYiiWebRequest::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 yiiwebApplication($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 yiiwebApplication($config))
— в конструкторе происходит присвоение Yii::$app = $this;
Компонент запроса выглядит так:
app/Yii/Web/Request.php
namespace AppYiiWeb;
class Request extends yiiwebRequest
{
public function getCsrfToken($regenerate = false)
{
return Session::token();
}
}
Токеном управляет Laravel, поэтому регенерацию обрабатывать не надо.
Грид
Теперь можно попробовать запустить. Добавим код для списка заказов.
app/Http/Controllers/Admin/OrderController.php
public function index(Request $request)
{
$allModels = Order::query()->get()->all();
$gridViewConfig = [
'dataProvider' => new yiidataArrayDataProvider([
'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' => yiigridActionColumn::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>
{!! yiigridGridView::widget($gridViewConfig) !!}
@endsection
Нужно установить dataProvider.pagination.route
и dataProvider.sort.route
, иначе произойдет обращение к Yii::$app->controller->getRoute()
, а контроллер у нас null
. Аналогично с ActionColumn
, только там будет проверка и InvalidParamException
. URL генерируется через yiiwebUrlManager
, но результат получается такой же, как с роутингом Laravel. Можно задать менеджер через dataProvider.pagination.urlManager
, если нужно.
Метки колонок пока оставим автогенерируемые.
Также надо задать некоторые стили для иконок сортировки.
Грид выводится, но так как нет фронтенд-скриптов, то кнопка Delete не работает.
Надо вывести скрипты, которые находятся в компоненте yiiwebView
. Методы renderHeadHtml(), renderBodyBeginHtml(), renderBodyEndHtml()
защищены (непонятно от кого, особенно учитывая, что все переменные public
). Как ни странно, есть повод применить антипаттерн «public morozov». Или можно просто скопипастить их в главный шаблон.
app/Yii/Web/View.php
namespace AppYiiWeb;
class View extends yiiwebView
{
public function getHeadHtml()
{
return parent::renderHeadHtml();
}
public function getBodyBeginHtml()
{
return parent::renderBodyBeginHtml();
}
public function getBodyEndHtml($ajaxMode = false)
{
return parent::renderBodyEndHtml($ajaxMode);
}
public function initAssets()
{
yiiwebYiiAsset::register($this);
ob_start();
$this->beginBody();
$this->endBody();
ob_get_clean();
}
}
В Yii регистрация ассетов происходит в функции endBody()
, а также весь рендеринг оборачивается в буфер, в котором потом производится замена магических констант CDATA
на реальные ассеты. Эмуляция этого поведения находится в функции initAssets()
. Заменять мы ничего не будем, нам нужно просто чтобы были заполнены свойства $this->js, $this->css
и другие.
'components' => [
...
'view' => [
'class' => AppYiiWebView::class,
],
],
<!DOCTYPE html>
<html lang="en">
<head>
...
<?php $view = Yii::$app->getView(); $view->initAssets(); ?>
{!! yiihelpersHtml::csrfMetaTags() !!}
{!! $view->getHeadHtml() !!}
</head>
<body>
{!! $view->getBodyBeginHtml() !!}
...
{!! $view->getBodyEndHtml() !!}
</body>
</html>
Вызов Html::csrfMetaTags()
нужен, так как скрипт yii.js берет csrf-токен из HTML страницы.
ArrayDataProvider
работает, но надо сделать аналог ActiveDataProvider
, чтобы получать из базы только то что нужно.
app/Yii/Data/EloquentDataProvider.php
class EloquentDataProvider extends yiidataBaseDataProvider
{
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 AppYiiDataEloquentDataProvider([
'query' => Order::query(),
'pagination' => ['route' => $request->route()->uri(), 'defaultPageSize' => 10],
'sort' => ['route' => $request->route()->uri(), 'attributes' => ['id']],
]),
Метки и фильтры
Нужно сделать базовую модель, унаследованную от yiibaseModel
, которая будет возвращать гриду метки для колонок и правила полей для фильтрации. Для этого есть параметр filterModel
. Сделаем ее конфигурируемой через конструктор.
namespace AppYiiData;
use AppYiiDataEloquentDataProvider;
use Route;
class FilterModel extends yiibaseModel
{
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 AppFormsAdmin;
use AppYiiDataFilterModel;
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 = AppModelsOrder::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 AppFormsAdminOrderFilter();
$dataProvider = $filterModel->applyFilter($request);
$gridViewConfig = [
'dataProvider' => $dataProvider,
'filterModel' => $filterModel,
...
];
...
}
Есть небольшая проблема, что если задана filterModel, но нет ни одного поля для фильтра, то все равно отображается строка с пустыми ячейками. В этом случае можно метки вручную проставить. Хотя лучше было бы, если бы в самом компоненте была такая проверка.
Просмотр
Тут делаем аналогично, настройки колонок можно скопировать из грида. Товары в заказе сделаем отдельным гридом на странице просмотра. Метки тоже пока оставим автогенерируемые.
app/Http/Controllers/Admin/OrderController.php
public function view($id)
{
$model = Order::findOrFail($id);
$detailViewConfig = [
'model' => $model,
'attributes' => [
'id',
'user.name',
'created_at:datetime',
'updated_at:datetime',
],
];
$gridViewConfig = [
'dataProvider' => new AppYiiDataEloquentDataProvider([
'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>
{!! yiiwidgetsDetailView::widget($detailViewConfig) !!}
<h2>Order Items</h2>
{!! yiigridGridView::widget($gridViewConfig) !!}
@endsection
Создание / Обновление
Сначала нужно сделать модель формы, враппер для Eloquent моделей, унаследованный от yiibaseModel
, чтобы компонент ActiveForm
мог вызывать нужные методы.
namespace AppYiiData;
use IlluminateDatabaseEloquentModel as EloquentModel;
class FormModel extends yiibaseModel
{
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.php
public function create(Request $request)
{
$model = new Order();
$formModel = new AppYiiDataFormModel(
$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 AppYiiDataFormModel(
$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 = yiiwidgetsActiveForm::begin() ?>
{!! $form->field($formModel, 'user_id')->dropDownList(AppUser::pluck('name', 'id'), ['prompt' => '']) !!}
<button type="submit" class="btn btn-primary">Submit</button>
<?php yiiwidgetsActiveForm::end() ?>
Правила валидации задаются в стиле Yii. Если нужно, можно переопределить метод validate()
и вызывать там валидатор Laravel. В данном примере мы этого делать не будем.
Blade не разрешает объявлять переменные. А ActiveForm::begin()
и выводит теги и возвращает значение. Можно явно написать тег <?php ?>
, можно сделать новый тег через Blade::extend()
, как советуют здесь, можно сделать обертку для ActiveForm
. Пока оставим <?php ?>
.
Как и в случае с фильтром, можно унаследоваться от FormModel
и поместить все объявления туда.
namespace AppFormsAdmin;
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 AppFormsAdminOrderForm($model);
$detailViewConfig = [
'model' => $formModel,
...
];
Удаление
Тут все просто.
app/Http/Controllers/Admin/OrderController.php
public 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' => yiidbConnection::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' => yiigiiModule::class];
$config['bootstrap'][] = 'gii';
}
(new yiiwebApplication($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('@'...)
, поэтому для класса AppModelsOrder
будет проверяться путь '@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 yiiwebApplication) {
Yii::$app->assetManager->bundles = [];
}
}
Можно вынести конфигурацию ActionColumn
в отдельный класс, чтобы не копировать в разные гриды.
namespace AppYiiWidgets;
use URL;
use Route;
class ActionColumn extends yiigridActionColumn
{
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 AppYiiWidgets;
use yiiwidgetsActiveForm;
use yiihelpersHtml;
class FormBuilder extends yiibaseComponent
{
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(
AppUser::pluck('name', 'id'),
['prompt' => ''])
!!}
{!! $form->submitButton('Submit'); !!}
{!! $form->close() !!}
Исходный код
Исходный код можно найти здесь. Все шаги сделаны отдельными коммитами. Есть миграции и тестовые данные.
php artisan migrate:refresh --seed
Обертки находятся в папке app/Yii
.
Обязательные:
AppYiiWebRequest
AppYiiDataEloquentDataProvider
AppYiiDataFormModel
Без остальных можно обойтись, но с ними удобнее:
AppYiiDataFilterModel
AppYiiWebView
AppYiiWidgetsActionColumn
AppYiiWidgetsFormBuilder
Также, думаю, это неплохой пример для сравнения разных реализаций. Если есть время и желание, приводите в комментариях свою реализацию этой админки на другом стеке технологий.
Автор: michael_vostrikov