Screen в Orchid — это не контроллер и не страница в привычном смысле. Это описание: какие данные нужны, из каких блоков состоит экран, какие действия доступны. Понимание этого разделения определяет, будет ли админка поддерживаемой или превратится в набор экранов на тысячу строк.
Разберём составные части и границу между ними.
Анатомия Screen
Screen — это класс, описывающий страницу админки:
class MyScreen extends Screen
{
// Данные для экрана
public function query(): iterable { }
// Кнопки в header
public function commandBar(): iterable { }
// Структура страницы
public function layout(): iterable { }
}Screen vs Controller: Screen объединяет логику контроллера и view в один файл. Меньше файлов, меньше boilerplate.
Query — загрузка данных
public function query(Order $order): iterable
{
return [
// Простые данные
'order' => $order->load(['items.product', 'user']),
// Коллекции для таблиц
'items' => $order->items,
// Метрики
'metrics' => [
'total' => $order->total,
'items_count' => $order->items->count(),
],
// Графики
'sales_chart' => Order::query()
->selectRaw('DATE(created_at) as date, SUM(total) as total')
->groupBy('date')
->orderBy('date')
->limit(30)
->get(),
];
}query() — единственное место, где загружаются данные, и оно выполняется при каждом открытии экрана. Отсюда два практических следствия.
Первое: все запросы экрана видны в одном методе — это удобно для оптимизации, но и вся стоимость экрана сосредоточена здесь. Забытый with() для связей превращается в N+1 при отрисовке таблицы, а несколько агрегатов подряд — в несколько полных сканирований.
Второе: не кладите в query() то, что нужно не всегда. Данные для вкладки, которую открывают раз в месяц, всё равно будут загружаться при каждом заходе на экран. Для таких случаев есть асинхронные модальные окна и отложенная загрузка.
Layouts — строительные блоки
Rows — вертикальный список полей
use Orchid\Support\Facades\Layout;
use Orchid\Screen\Fields\Input;
use Orchid\Screen\Fields\Select;
use Orchid\Screen\Fields\DateTimer;
public function layout(): iterable
{
return [
Layout::rows([
Input::make('order.number')
->title('Номер заказа')
->readonly(),
Select::make('order.status')
->title('Статус')
->options([
'pending' => 'Ожидает',
'processing' => 'В обработке',
'shipped' => 'Отправлен',
'delivered' => 'Доставлен',
]),
DateTimer::make('order.shipped_at')
->title('Дата отправки')
->format('Y-m-d'),
]),
];
}Columns — колонки
public function layout(): iterable
{
return [
Layout::columns([
// Левая колонка (8/12)
Layout::rows([
Input::make('post.title')->title('Заголовок'),
Quill::make('post.content')->title('Содержание'),
]),
// Правая колонка (4/12)
Layout::rows([
Switcher::make('post.is_published')->title('Опубликовать'),
DateTimer::make('post.published_at')->title('Дата публикации'),
Select::make('post.category_id')
->fromModel(Category::class, 'name')
->title('Категория'),
]),
]),
];
}Tabs — вкладки
public function layout(): iterable
{
return [
Layout::tabs([
'Основное' => [
Layout::rows([
Input::make('product.name')->title('Название'),
TextArea::make('product.description')->title('Описание'),
]),
],
'Цены' => [
Layout::rows([
Input::make('product.price')->title('Цена')->type('number'),
Input::make('product.old_price')->title('Старая цена')->type('number'),
]),
],
'SEO' => [
Layout::rows([
Input::make('product.meta_title')->title('Meta Title'),
TextArea::make('product.meta_description')->title('Meta Description'),
]),
],
'Склад' => $this->stockLayout(),
]),
];
}
private function stockLayout(): array
{
return [
Layout::rows([
Input::make('product.stock')->title('Остаток')->type('number'),
Input::make('product.sku')->title('Артикул'),
]),
];
}Accordion — аккордеон
public function layout(): iterable
{
return [
Layout::accordion([
'Информация о клиенте' => [
Layout::rows([
Input::make('order.user.name')->title('Имя')->readonly(),
Input::make('order.user.email')->title('Email')->readonly(),
]),
],
'Адрес доставки' => [
Layout::rows([
Input::make('order.address.city')->title('Город'),
Input::make('order.address.street')->title('Улица'),
]),
],
]),
];
}Table — таблицы
use Orchid\Screen\TD;
use Orchid\Screen\Actions\Button;
use Orchid\Screen\Actions\DropDown;
use Orchid\Screen\Actions\Link;
public function layout(): iterable
{
return [
Layout::table('items', [
TD::make('product.name', 'Товар')
->render(fn($item) => $item->product->name),
TD::make('quantity', 'Кол-во')
->align(TD::ALIGN_CENTER),
TD::make('price', 'Цена')
->render(fn($item) => number_format($item->price, 0, ',', ' ') . ' ₽'),
TD::make('total', 'Сумма')
->render(fn($item) => number_format($item->price * $item->quantity, 0, ',', ' ') . ' ₽'),
TD::make('actions', 'Действия')
->align(TD::ALIGN_RIGHT)
->render(fn($item) => DropDown::make()
->icon('options-vertical')
->list([
Link::make('Редактировать')
->route('platform.item.edit', $item)
->icon('pencil'),
Button::make('Удалить')
->method('removeItem', ['item' => $item->id])
->icon('trash')
->confirm('Удалить позицию?'),
])
),
]),
];
}Modals — модальные окна
use Orchid\Screen\Actions\ModalToggle;
public function commandBar(): iterable
{
return [
ModalToggle::make('Добавить позицию')
->modal('addItemModal')
->icon('plus')
->method('addItem'),
];
}
public function layout(): iterable
{
return [
// Основной контент
Layout::table('items', [...]),
// Модальное окно
Layout::modal('addItemModal', [
Layout::rows([
Select::make('product_id')
->fromModel(Product::class, 'name')
->title('Товар')
->required(),
Input::make('quantity')
->title('Количество')
->type('number')
->value(1)
->required(),
]),
])
->title('Добавить позицию')
->applyButton('Добавить'),
];
}
public function addItem(Request $request): void
{
$validated = $request->validate([
'product_id' => 'required|exists:products,id',
'quantity' => 'required|integer|min:1',
]);
// Логика добавления...
Toast::success('Позиция добавлена');
}Async Modal — загрузка данных
public function commandBar(): iterable
{
return [
ModalToggle::make('Просмотр')
->modal('viewModal')
->modalTitle('Детали заказа')
->async('asyncGetOrder'), // Метод для загрузки данных
];
}
public function asyncGetOrder(Order $order): iterable
{
return [
'order' => $order->load('items'),
];
}
public function layout(): iterable
{
return [
Layout::modal('viewModal', [
Layout::rows([
Input::make('order.number')->readonly(),
Input::make('order.total')->readonly(),
]),
])->async('asyncGetOrder'),
];
}Listeners — реактивность
public function layout(): iterable
{
return [
Layout::rows([
Select::make('order.delivery_type')
->options([
'pickup' => 'Самовывоз',
'courier' => 'Курьер',
'post' => 'Почта',
])
->title('Способ доставки'),
// Показываем только для курьера
Input::make('order.address')
->title('Адрес доставки')
->canSee($this->query['order']->delivery_type === 'courier'),
]),
];
}Turbo-обновление при изменении
Layout::rows([
Select::make('category_id')
->fromModel(Category::class, 'name')
->title('Категория'),
// Это поле обновится при смене категории
Relation::make('subcategory_id')
->fromModel(Subcategory::class, 'name')
->applyScope('forCategory', $this->query['category_id'])
->title('Подкатегория'),
])->async('asyncUpdateSubcategories'),Кастомный Layout
// app/Orchid/Layouts/OrderSummaryLayout.php
namespace App\Orchid\Layouts;
use Orchid\Screen\Layouts\Rows;
use Orchid\Screen\Fields\Label;
class OrderSummaryLayout extends Rows
{
protected function fields(): iterable
{
$order = $this->query->get('order');
return [
Label::make('summary')
->title('Итого по заказу')
->value(view('admin.partials.order-summary', compact('order'))),
];
}
}
// Использование
public function layout(): iterable
{
return [
Layout::table('items', [...]),
OrderSummaryLayout::class,
];
}Производительность: Не загружайте много данных в query(). Используйте пагинацию и ленивую загрузку для связей.
Итоги
| Layout | Назначение |
|---|---|
rows | Вертикальный список полей |
columns | Колонки (grid) |
table | Таблица с данными |
tabs | Вкладки |
accordion | Аккордеон |
modal | Модальное окно |
block | Блок с заголовком |
legend | Группировка полей |
Правила, к которым сводится работа с экранами.
Разделение обязанностей соблюдайте буквально. query() — только загрузка данных, layout() — только структура, обработчики — только действия. Бизнес-логика в обработчике экрана — та же ошибка, что логика в контроллере: её нельзя вызвать из консольной команды, из очереди или из другого экрана. Обработчик должен быть тонким: проверить права, разобрать вход, вызвать сервис.
Выносите повторяющиеся layouts в классы. Одинаковая таблица заказов на трёх экранах — это один класс, а не три копии. Копии расходятся через месяц, и расхождение обнаруживает пользователь.
Разбивайте на вкладки, но помните про стоимость. Вкладки организуют интерфейс, а не уменьшают нагрузку: данные всех вкладок загружаются в query() одновременно. Тяжёлой вкладке нужна асинхронная загрузка, а не просто отдельная вкладка.
Не перегружайте один экран. Признак, по которому это видно: query() длиннее тридцати строк или больше пяти блоков верхнего уровня в layout(). Обычно это значит, что экран пытается быть двумя.
Про модальные окна. Асинхронные модальные окна — самый недооценённый инструмент Orchid: они позволяют не тащить данные редко используемых форм в основной запрос экрана. Если экран медленный, а половина его содержимого — формы, которые открывают изредка, перенос их в асинхронные модальные окна даёт больше, чем любая оптимизация запросов.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.