Виртуальные страницы
Страница со своим адресом, которой нет в дереве ресурсов MODX. Маршрут, данные и шаблон ваши; MODX только приносит запрос.
Когда это нужно
- Каталог на тысячи позиций, где ресурс на каждую позицию — абсурд
- Профили пользователей —
/user/boshnik - Статьи из своей таблицы, а не из
site_content - Фильтры в адресе —
/catalog/sale/smartphones - Эндпоинты API —
/api/products/42
Страница товара, целиком
1. Маршрут
Файлы маршрутов лежат в core/App/routes/ и подключаются глобом по алфавиту. Адрес пишется без ведущего слэша:
<?php
use Boshnik\PageBlocks\Facades\Route;
Route::get('product/{alias}', 'ProductController@show')->name('product.show');Порядок по алфавиту решает, кто победит
web.php обычно заканчивается Route::fallback(). Файл, который сортируется раньшеweb.php, будет проверен первым; тот, что после, не получит шанса вовсе. Назвать файл product.php — не косметика.
2. Контроллер
core/App/Http/Controllers/ProductController.php. Параметры маршрута идут первыми, запрос последним:
<?php
namespace PageBlocks\App\Http\Controllers;
use Boshnik\PageBlocks\Http\Request;
use PageBlocks\App\Models\PbProduct;
class ProductController extends BaseController
{
public function show(string $alias, Request $request)
{
$product = PbProduct::query()
->where('alias', $alias)
->whereNotNull('published_at')
->first();
if (!$product) {
abort(404);
}
$this->shell($product->name, $product->description);
return view('file:templates/product', [
'product' => $product,
]);
}
}3. Шаблон
core/App/elements/templates/product.tpl:
<h1>{$product->name}</h1>
<div>{$product->description}</div>
<p>Цена: {$product->price}</p>Какой базовый класс
Их два, и выбор решает, придётся ли вообще думать об оболочке:
| Базовый класс | Что делает за вас |
|---|---|
Controller | Резолвит текущий ресурс MODX по адресу, алиасу и контексту и разворачивает его properties. Берите, когда странице соответствует ресурс. |
BaseController | Ничего подобного. Берите для страницы, у которой ресурса нет, — то есть для виртуальной. |
У виртуальной страницы резолвить нечего, поэтому честное сочетание — BaseController плюс оболочка ниже.
То, о чём никто не предупреждает: оболочка
У виртуальной страницы нет ресурса, а значит $modx->resource не ваш — и шапка, меню, хлебные крошки и все мета-теги в вёрстке читают именно из него. Отрендерите как есть — страница выйдет без шапки или с чужой.
Рабочий ответ — занять ресурс под оболочку и перезаписать поля, описывающие страницу:
private function shell(string $title, string $description = ''): void
{
// Раздел, к которому относится страница. Запасной вариант — главная:
// страница с чужой шапкой лучше страницы без шапки.
$resource = Resource::find(4) ?: Resource::find((int) config('site_start', 1));
if (!$resource) {
return;
}
$this->modx->resource = $resource;
$this->modx->resource->pagetitle = $title;
$this->modx->resource->seo_title = $title;
$this->modx->resource->longtitle = $title;
$this->modx->resource->description = $description;
$this->modx->resource->seo_desc = $description;
$this->modx->resource->content = '';
$this->modx->resource->uri = 'product/' . $alias;
}uri перезаписывать обязательно
Оставите — и канонический адрес будет указывать на занятый ресурс: страница сама сообщает поисковику, что она копия раздела, и в индекс не попадает никогда. То же с description: страница берёт у раздела оболочку, но не его обещание.
Постраничность и 404
Виртуальный список обязан сам отвечать 404 за последней страницей. За вас этого не сделает никто:
if ($paginator->currentPage() > $paginator->lastPage()) {
abort(404);
}Без этого ?page=999 отдаёт 200 и пустую таблицу — для поискового робота это ещё одна пустая страница раздела.
Что вы получаете
- Независимость от дерева ресурсов — структура ваша
- Любую форму адреса, с параметрами
- Контроль доступа, редиректы и обработку ошибок обычным PHP
- Данные откуда угодно: свои таблицы, API, кэш