На прошлых уроках мы работали с миграциями Phinx и моделями Eloquent - это всё сторонние библиотеки, а где же сам Vesp?
А он в контроллерах, которые были написаны под сильным впечатлением от процессоров MODX, и работают через Slim 4. Интересный факт - MODX 3 в своё время планировали завязать именно на Slim, но что-то пошло не так, и эти планы забросили.
Итак, все запросы от пользователей приходят в один-единственный коннектор www/api.php
. Он подключает всё нужное файлом core/bootstrap.php
(который можно использовать во всяких консольных скриптах) и создаёт экземпляр приложения Slim.
Дальше в это приложение грузятся наши маршруты из файла core/routes.php
, и теперь система знает, какой контроллер отвечает за конкретный запрос. Давайте разберёмся с маршрутами.
Первым делом смотрим в core/routes.php
:
<?php
use Slim\Routing\RouteCollectorProxy;
// Все маршруты начинаются с /api/...
$group = $app->group(
'/api',
function (RouteCollectorProxy $group) {
// А дальше простое перечисление кто за что отвечает
$group->any('/security/login', App\Controllers\Security\Login::class);
$group->any('/security/logout', App\Controllers\Security\Logout::class);
$group->any('/user/profile', App\Controllers\User\Profile::class);
// Контроллер вывода загруженных изображений
$group->get('/image/{id}', App\Controllers\Image::class);
// Контроллеры админки - группа admin внутри группы api
$group->group(
'/admin',
static function (RouteCollectorProxy $group) {
$group->any('/users[/{id}]', App\Controllers\Admin\Users::class);
$group->any('/user-roles[/{id}]', App\Controllers\Admin\UserRoles::class);
}
);
}
);
Для примера, все запросы на авторизацию должны отправляться в /api/security/login
и принимать их будет контроллер App\Controllers\Security\Login
.
Так как Slim 4 использует для маршрутизации FastRoute, мы можем использовать его шаблоны для указания адресов.
$group->any('/users[/{id}]', App\Controllers\Admin\Users::class);
Эта запись означает, что контроллер App\Controllers\Admin\Users
отвечает и за /api/users
(вывод всех пользователей) и за /api/user/10
(вывод конкретного пользователя с id = 10). Более подробно о правилах маршрутов можно почитать в документации FastRoute.
Тема может быть довольно сложной, поэтому давайте чуть отвлечёмся на практическую часть.
Создаём новый контроллер по пути core/src/Controllers/Web/Users.php
:
<?php
// Наше новое пространство имён
namespace App\Controllers\Web;
use App\Models\User;
use Vesp\Controllers\ModelGetController;
// Контроллер должен расширять контроллеры из Vesp
class Users extends ModelGetController
{
// Указываем с какой моделью контроллер работает
protected $model = User::class;
}
Контроллер есть, но мы не сможем его открыть, потому что система не знает, какой адрес он обслуживает. Нужно отредактировать core/routes.php
. Добавляем новую группу сразу после admin
:
$group->group(
'/web',
static function (RouteCollectorProxy $group) {
// Вот и наш контроллер, отвечает только на GET запросы
$group->get('/users[/{id}]', App\Controllers\Web\Users::class);
}
);
Теперь переходим по адресу http://vesp-shop.test/api/web/users и видим наших пользователей, а таже общее количество записей - у меня 4 штуки.
Более того, можно посмотреть каждого пользователя отдельно, добавив в адрес его id: http://vesp-shop.test/api/web/users/1
Довольно просто, не так ли? Теперь давайте посмотрим за счёт чего эта магия работает.
Все контроллеры Vesp наследуются от одного, основного - Vesp\Controllers\Controller. В нём есть самые основные методы:
scope
- строка с требуемым разрешением на запуск, по умолчанию пустаяgetProperty()
- получает переданные параметры по ключу, например $this->getProperty('id') или $this->getProperty('limit')setProperty()
- устанавливает параметр, можно заменять присланные параметры пользователя своимиunsetProperty()
- удаляет установленный параметр, например если юзеру нельзя указывать limit
getProperties()
- возвращает весь массив с параметрами контроллераsetProperties()
- заменяет все параметры своим массивомresponse()
- базовый метод ответа на запрос пользователю в формате JSONsuccess()
- ответ об успешной операции, использует response()
с заранее указанным кодом ответа 200
failure()
- тоже сокращённый вызов response()
с ответом о неудачном запросе, код по умолчанию 422
.checkScope()
- метод для проверки прав пользователя__invoke()
- основной метод работы контроллераМетод __invoke()
является магическим и выполняется при срабатывании маршрута. Он выставляет в контроллере все нужные свойства:
protected $route
- текущий маршрут, наследник интерфейса Slim\Interfaces\RouteInterface
protected $request
- объект с данными запроса пользователя, наследник Psr\Http\Message\RequestInterface
protected $response
- наследник Psr\Http\Message\ResponseInterface
, используется в методе response()
для вывода JSON ответаprotected $user
- класс с текущим пользователем App\Models\User
, который сделал запрос. Если юзер не авторизовался, то будет nullПосле сохранения всех этих нужных штук Controller, использую $this->request определяет, а каким методом поступил запрос: OPTIONS
, GET
, POST
, PUT
или PATCH
- и смотрит, есть ли соответствующая функция в контроллере? И если есть - передаёт работу ей.
То есть, приходит запрос GET
, контроллер пытается запустить $this->get()
. Пришёл запрос POST
- будет запущен метод $this->post()
- очень просто.
Если нужной функции нет, юзер получит ошибку 405 Method Not Allowed
.
По умолчанию в Controller
прописан ровно один метод - options()
:
public function options(): ResponseInterface
{
$response = $this->success();
if (getenv('CORS')) {
$response = $response
->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Authorization')
->withHeader('Access-Control-Allow-Methods', 'POST, GET, HEAD, OPTIONS, DELETE, PUT, PATCH, UPDATE');
}
return $response;
}
Этот метод отвечает на кросс-доменные запросы и, если параметр CORS
в файле .env
не выключен, то добавляет разрешающие заголовки.
Думаю, каждый разработчик встречался с запретом кросдоменных запросов в браузере, когда пытался обратиться через Ajax на другой домен. Так вот, Vesp по умолчанию отвечает на эти запросы без ошибок.
Как видите, базовый контроллер не работает с моделями, а только проверяет права, выставляет нужные свойства и передаёт дальнейшую работу функции, отвечающий за метод запроса.
А для работы с моделями у нас есть второй, самый популярный контроллер - Vesp\Controllers\ModelController, именно его почти всегда раширяют контроллеры админки.
Здесь добавляются новые свойства:
protected $model
- обязательная модель этого контроллера, с которой он работаетprotected $primaryKey = 'id'
- первичный ключ моделиprotected $maxLimit = 1000
- максимальное количество выбираемых строк через GET, можно отключить, указав 0 - нет лимитаДальше идут основные методы работы с моделью:
put()
- создание новой модели, то есть сохранение новой записи в её таблиц базы данныхpatch()
- изменение модели по её первичному ключуdelete()
- удаление модели по ключуget()
- получение одной или нескольких моделей для просмотра, если количество результатов больше $this->maxLimit
, то ограничивает их до этого числаА теперь вспомогательные методы, которые вызываются из основных:
beforeSave()
- проверка свойств модели перед сохранением. Здесь можно вернуть ошибку, типа "такое-то поле не заполнено" через $this->failure()
или null
, если всё в порядке.
afterSave()
- модификация уже сохранённой модели, можно довыбирать к ней какие-то данные перед возвращением юзеру. Возвращает модель, никаких ошибок тут уже нет.
beforeGet()
- метод модификации выборки одной модели по первиному ключу
beforeCount()
- метод модификации выборки коллекции моделей, когда первичного ключа не указано. Вызывается перед подсчётом итогового количества выбираемых записей, поэтому именно здесь нужно указывать условия where()
, join()
и т.д.
afterCount()
- метод для добавления в выборку сортировки, связей моделей и прочего, что не влияет на количество выбираемых моделей.
prepareRow()
- метод, через который проходит каждая модель перед выводом наружу пользователю. Превращает класс модели в массив, используется и в put()
и в patch()
, и конечно же, в get()
. В этом методе вы можете стандартизировать что именно вернётся юзеру в ответ на любой запрос.
beforeDelete()
- проверка модели перед удалением, можно вернуть null
если всё ок, или $this->failure()
- если нет.
Есть еще несколько служебных методов для указания сортировки и прочего, но нам они пока неинтересны.
Остался последний, самый просто контроллер Vesp\Controllers\ModelGetController. Она расширяет ModelController
и отключает все методы, кроме get()
. Таким образом именно его удобнее всего использовать для публичных контроллеров без авторизации, когда пользователи не могут менять модели.
Давайте теперь вернёмся к нашему публичному контроллеру с пользователями core/src/Controllers/Web/Users.php
, и добавим в него несколько условий:
<?php
namespace App\Controllers\Web;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Vesp\Controllers\ModelGetController;
class Users extends ModelGetController
{
protected $model = User::class;
// Меняем уловия перед подсчётом результатов
protected function beforeCount(Builder $c): Builder
{
// Выбираем только активных пользователей
$c->where('active', true);
// Если прислали параметр query - это поиск
if ($query = $this->getProperty('query')) {
// Добавляем вложенное условие для фильтрации по username и fullname
$c->where(static function (Builder $c) use ($query) {
$c->where('username', 'LIKE', "%$query%");
$c->orWhere('fullname', 'LIKE', "%$query%");
});
}
// И возвращаем модифицированный запрос
return $c;
}
// Меняем запрос после подсчёта результатов
protected function afterCount(Builder $c): Builder
{
// Если не указана сортировка - сортируем по username
if (!$this->getProperty('sort')) {
$c->orderBy('username');
}
return $c;
}
// Ну и подготовка ответа
public function prepareRow(Model $object): array
{
/** @var User $object */
// Здесь выбираем только 3 колонки из всей модели
$array = [
'id' => $object->id,
'username' => $object->username,
'fullname' => $object->fullname,
];
// И возвращаем массив
return $array;
}
}
Проверяем:
username
. user
в username
или fullname
- http://vesp-shop.test/api/web/users?query=user created_at
- http://vesp-shop.test/api/web/users?sort=created_at&dir=descКак видите, расширять базовые контроллеры очень весело. Вы прописываете только то, что нужно вам, остальное работает по-умолчанию.
Урок получился большой и сложный, но это база, без которой дальше никуда. Вся работа стротся на том, что мы будем создавать новые модели и контроллеры для работы с ними, которые будут вызываться с фронтенда пользователями. Так что нужно хорошо понимать, как это всё работает.
На следующем уроке мы узнаем как работает авторизация пользователя и проверка разрешений в контроллере.
Пишет такую ошибку: Fatal error: Could not check compatibility between App\Controllers\Web\Users::beforeCount(App\Controllers\Web\Builder $c): App\Controllers\Web\Builder and Vesp\Controllers\ModelController::beforeCount(Illuminate\Database\Eloquent\Builder $c): Illuminate\Database\Eloquent\Builder, because class App\Controllers\Web\Builder is not available in /Users/max/projects/vesp/VespShop/core/src/Controllers/Web/Users.php on line 17
а на 17 строке у меня объявление метода:
Моя вина, не импортировал в контроллер нужные классы, поэтому PHP не может правильно использовать класс
Builder
, на что и ругается.Должно быть вот так:
<?php namespace App\Controllers\Web; use App\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Vesp\Controllers\ModelGetController; class Users extends ModelGetController { // .... }
Обновил заметку, спасибо за замечание!
Спасибо за оперативный ответ)
Я вижу "Not found." 404 ошибка и логи пустые.
http://vesp-shop.test/api+/web/users
, то вижу заглушку от Nuxt.А ты не забыл, случайно, прописать новый маршрут в
core/routes.php
?Спасибо Василий!
Я подумал, что уже все за меня создано и нужно только перейти по адресу )).
Создал контроллер Users.php, прописал маршрут в core/routes.php и все заработало.
Круто все спроектировано!
На здоровье!
Василий, я пытаюсь курс. Но из-за того, что мне сложно на Винде воспроизвести все необходимое окружение, я начал не по уроку "Создание нового проекта", где VespShop ставится из Packagist при помощи composer. А я просто продолжил работу в Докере, создав сначала проект по заметке "Vesp в Docker".
И первый вопрос в том - можно ли так делать, или есть различия в исходниках?
По этому уроку я выполнил практическую часть и получил все твои выводы по адресам:
Для этого создал директорию Web в core/src/Controllers и там создал Users.php, который затем и расширил, как здесь написано. Т.е. все работает. Но только в этом уроке ты пишешь о контроллерах Controller.php, ModelController.php и ModelGetController.php в Vesp\Controllers. Эти файлы присутствуют в https://github.com/bezumkin/vesp-core/tree/master/src/Controllers . А вот в моей "докерном" проекте этих файлов нет. Хотя я конечно могу их взять из твоего репозитория). Но вопрос в том - нужны ли они?
Базовые контроллеры у тебя уже установлены как зависимость composer, иначе ничего бы не работало.
Можешь их поискать в
/core/vendor/vesp
Ок, вижу
\core\vendor\vesp\core\src\Controllers
Спасибо!