Lattice — це SDUI-фреймворк (Server-driven UI) для Laravel, який дозволяє описувати інтерфейс сторінок, форм, таблиць та меню за допомогою PHP. Завдяки Inertia ці структури рендеряться як реальні React-компоненти. З Lattice сервер стає єдиним джерелом істини (single source of truth) для опису екранів, залишаючи клієнту лише одне завдання — їхнє відображення.
Кожна сторінка — це PHP-клас, що будує дерево компонентів. Lattice серіалізує це дерево у типізований payload і передає його через Inertia як звичайний візит. На фронтенді єдиний React-компонент зіставляє кожен вузол із реєстром компонентів і малює інтерфейс.
Ключові переваги такої моделі:
- Автоматичний роутинг сторінок-класів. Атрибут
#[AsPage]реєструє маршрут автоматично — Lattice сканує вказані шляхи та підключає їх без ручного внесення в routes. - Форми з нативною валідацією Laravel. Поля описуються в PHP, перевіряються стандартними правилами Laravel, а за потреби підтримують «живу» валідацію через Precognition.
- Таблиці на базі Eloquent. Колонки, сортування, фільтрація та пагінація визначаються у класі таблиці, який повертає query builder.
- Серверні дії (Actions) з ефектами. При кліку виконується PHP-код, який повертає клієнту інструкції: показати toast-сповіщення, виконати редирект або оновити компонент.
# Сторінки
Сторінка успадковує базовий клас Page, використовує атрибут #[AsPage] для роутингу та будує UI у методі render(). Замість JSX дерево компонентів збирається за допомогою ланцюжкових викликів PHP: Stack, Grid, Heading, Card тощо.
use Lattice\Lattice\Attributes\AsPage;
use Lattice\Lattice\Core\Components\Card;
use Lattice\Lattice\Core\Components\Grid;
use Lattice\Lattice\Core\Components\Heading;
use Lattice\Lattice\Core\Components\Stack;
use Lattice\Lattice\Core\Components\Text;
use Lattice\Lattice\Core\Enums\Gap;
use Lattice\Lattice\Core\PageSchema;
use Lattice\Lattice\Http\Page as BasePage;
#[AsPage(route: '/dashboard', middleware: ['web'])]
final class DashboardPage extends BasePage
{
public function title(): string
{
return 'Dashboard';
}
public function render(PageSchema $schema): PageSchema
{
return $schema->schema([
Stack::make('dashboard')
->gap(Gap::Large)
->schema([
Heading::make('Dashboard'),
Text::make('Everything below is described in PHP and rendered as React.'),
Grid::make('stats')
->columns(2)
->schema([
Card::make('Orders', '128 this week.'),
Card::make('Revenue', '$4,210 this week.'),
]),
]),
]);
}
}
Параметри маршруту передаються безпосередньо в render() через route-model binding. Також можна використовувати метод authorize() для перевірки прав доступу перед рендерингом:
#[AsPage(route: '/products/{product}/edit')]
class ProductEditPage extends Page
{
public function authorize(Request $request): bool
{
return $request->user()?->can('update', Product::class) ?? false;
}
public function render(PageSchema $schema, Product $product): PageSchema
{
return $schema->schema([
Heading::make("Edit {$product->name}"),
]);
}
}
# Форми
Форма — це клас FormDefinition, де описуються поля та логіка обробки. Lattice рендерить React-інпути, валідує запит за правилами Laravel і виконує handle() після успішного сабміту:
use Illuminate\Http\Request;
use Lattice\Lattice\Attributes\AsForm;
use Lattice\Lattice\Forms\Components\Form as FormComponent;
use Lattice\Lattice\Forms\Components\TextInput;
use Lattice\Lattice\Forms\FormDefinition;
use Symfony\Component\HttpFoundation\Response;
#[AsForm('app.profile.form')]
class ProfileForm extends FormDefinition
{
public function definition(FormComponent $form, Request $request): FormComponent
{
return $form->schema([
TextInput::make('name', 'Name')->rules(['required', 'string', 'max:255']),
TextInput::make('email', 'Email')->email()->rules(['required', 'email']),
]);
}
public function handle(Request $request): Response
{
$validated = $this->validate($request);
$request->user()->update($validated);
return redirect('/profile');
}
}
Додати форму на сторінку можна одним викликом, гнучко налаштовуючи HTTP-метод, текст кнопки та початкові дані. Опція ->precognitive(500) активує «живу» перевірку помилок:
Form::use(ProfileForm::class)
->method(HttpMethod::Patch)
->submitLabel('Save changes')
->precognitive(500)
->fill([
'name' => $user->name,
'email' => $user->email,
]);
# Таблиці
Таблиці розширюють EloquentTableDefinition. Ви визначаєте колонки та повертаєте query builder, а Lattice автоматично бере на себе сортування, фільтрацію та пагінацію для полів, позначених як sortable() чи filterable():
use Illuminate\Database\Eloquent\Builder;
use Lattice\Lattice\Attributes\AsTable;
use Lattice\Lattice\Tables\Columns\BooleanColumn;
use Lattice\Lattice\Tables\Columns\NumberColumn;
use Lattice\Lattice\Tables\Columns\TextColumn;
use Lattice\Lattice\Tables\EloquentTableDefinition;
use Lattice\Lattice\Tables\TableQuery;
#[AsTable('app.products')]
class ProductsTable extends EloquentTableDefinition
{
public function columns(): array
{
return [
TextColumn::make('name')->sortable()->filterable(),
NumberColumn::make('price')->sortable()->filterable(),
BooleanColumn::make('featured'),
TextColumn::make('updated_at')->date('Y-m-d')->sortable(),
];
}
public function builder(TableQuery $query): Builder
{
return Product::query();
}
}
Рендеринг у компоненті використовує той самий підхід ::use(), що й у формах:
$schema->schema([
Heading::make('Products'),
Table::use(ProductsTable::class),
]);
# Actions та клієнтські ефекти
Actions дозволяють виконувати серверний код у відповідь на дії користувача. Клас ActionDefinition описує кнопку в definition() та виконує логіку в handle(). Замість повернення view, він повертає ActionResult з «ефектами» — командами для клієнта (наприклад, показати сповіщення або оновити частину сторінки):
use Illuminate\Http\Request;
use Lattice\Lattice\Actions\ActionDefinition;
use Lattice\Lattice\Actions\ActionResult;
use Lattice\Lattice\Actions\Components\Action;
use Lattice\Lattice\Attributes\AsAction;
use Lattice\Lattice\Core\Enums\ButtonVariant;
use Lattice\Lattice\Core\Enums\Variant;
#[AsAction('app.products.archive')]
class ArchiveProductAction extends ActionDefinition
{
public function definition(Action $action): Action
{
return $action
->label('Archive')
->variant(ButtonVariant::Destructive)
->confirm('Archive product?', 'This hides it from the catalogue.');
}
public function handle(Request $request): ActionResult
{
$product = $this->product($request);
$product->update(['status' => 'archived']);
return ActionResult::success()
->toast(Variant::Success, 'Product archived.')
->reloadComponent('app.products');
}
}
Прив’язана до рядка таблиці дія автоматично передає контекст на сервер, щоб handle() знав, з яким записом працювати:
Action::use(ArchiveProductAction::class)
->context(['product_id' => $row['id']]);
# Дізнатися більше
📕 Інструкції зі встановлення, довідник компонентів та налаштування тем доступні в документації.
💻 Вихідний код проєкту відкрито на GitHub.