dev
This commit is contained in:
parent
e6e8844f2b
commit
b3c8188ee3
52
App/Controller/FeedbackController.php
Normal file
52
App/Controller/FeedbackController.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package Bicycle
|
||||||
|
* @author Egor Isaev
|
||||||
|
* @description FeedbackController.php
|
||||||
|
* @copyright (c) 23/06/2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use System\Classes\Controller;
|
||||||
|
use System\Classes\Request;
|
||||||
|
use System\Classes\Validation;
|
||||||
|
use System\Classes\HTTP\Request as HTTPRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Демонстрация формы с валидацией.
|
||||||
|
* CSRF проверяется автоматически в Controller::before().
|
||||||
|
* Один экшен и показывает форму (GET), и обрабатывает её (POST).
|
||||||
|
*/
|
||||||
|
class FeedbackController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
* @throws \System\Classes\MyException
|
||||||
|
*/
|
||||||
|
public function indexAction(): string
|
||||||
|
{
|
||||||
|
$request = Request::$current;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($request->method() === HTTPRequest::POST) {
|
||||||
|
// CSRF уже проверен в BaseController::before(). Здесь — только данные.
|
||||||
|
$validation = Validation::factory($request->post())
|
||||||
|
->label('email', 'E-mail')
|
||||||
|
->label('message', 'Сообщение')
|
||||||
|
->rule('email', 'required')
|
||||||
|
->rule('email', 'email')
|
||||||
|
->rule('message', 'required')
|
||||||
|
->rule('message', 'min_length', [10]);
|
||||||
|
|
||||||
|
if ($validation->check()) {
|
||||||
|
// Данные чистые — здесь было бы сохранение/отправка письма.
|
||||||
|
return $this->render('feedback_ok', ['email' => $request->post('email')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = $validation->errors();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('feedback', ['errors' => $errors]);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
App/view/Feedback/feedback.html
Normal file
37
App/view/Feedback/feedback.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package Bicycle
|
||||||
|
* @author Egor Isaev
|
||||||
|
* @description feedback.html
|
||||||
|
* @copyright (c) 23/06/2026
|
||||||
|
*
|
||||||
|
* @var array $errors Ошибки валидации [поле => сообщение]
|
||||||
|
*/
|
||||||
|
|
||||||
|
use System\Classes\CSRF;
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<form method="post" action="/feedback">
|
||||||
|
<?= CSRF::field() ?>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label>E-mail<br>
|
||||||
|
<input type="email" name="email">
|
||||||
|
</label>
|
||||||
|
<?php if (isset($errors['email'])): ?>
|
||||||
|
<span style="color:red"><?= $errors['email'] ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label>Сообщение<br>
|
||||||
|
<textarea name="message" rows="4"></textarea>
|
||||||
|
</label>
|
||||||
|
<?php if (isset($errors['message'])): ?>
|
||||||
|
<span style="color:red"><?= $errors['message'] ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button type="submit">Отправить</button>
|
||||||
|
</form>
|
||||||
13
App/view/Feedback/feedback_ok.html
Normal file
13
App/view/Feedback/feedback_ok.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package Bicycle
|
||||||
|
* @author Egor Isaev
|
||||||
|
* @description feedback_ok.html
|
||||||
|
* @copyright (c) 23/06/2026
|
||||||
|
*
|
||||||
|
* @var string $email Адрес, с которого пришло сообщение
|
||||||
|
*/
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<p>Спасибо! Сообщение от <?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?> принято.</p>
|
||||||
53
CLAUDE.md
53
CLAUDE.md
@ -74,7 +74,8 @@ PhpStorm может показывать предупреждение «Namespac
|
|||||||
| Класс | Роль |
|
| Класс | Роль |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `System\Classes\Core` | Bootstrap, обработка ошибок, поиск файлов (`findFile`), константы среды |
|
| `System\Classes\Core` | Bootstrap, обработка ошибок, поиск файлов (`findFile`), константы среды |
|
||||||
| `System\Classes\Controller` | Базовый контроллер; `render()` строит двухуровневый вывод: layout + content |
|
| `System\Classes\BaseController` | Голое ядро (abstract); жизненный цикл `executeAction()`: `before()` → экшен → `after()` |
|
||||||
|
| `System\Classes\Controller` | Веб-контроллер (extends BaseController): `render()` + авто-CSRF в `before()` + `json()` |
|
||||||
| `System\Classes\View` | Рендеринг `.html`-шаблонов через `ob_start` + `extract` + `include` |
|
| `System\Classes\View` | Рендеринг `.html`-шаблонов через `ob_start` + `extract` + `include` |
|
||||||
| `System\Classes\Route` | Автоматический роутинг: парсит URI, ищет контроллер в `App/Controller/` |
|
| `System\Classes\Route` | Автоматический роутинг: парсит URI, ищет контроллер в `App/Controller/` |
|
||||||
| `System\Classes\Request` | Входящий HTTP-запрос; реализует `HTTP\Request`; `post()`, `query()`, `body()`, `isAjax()` |
|
| `System\Classes\Request` | Входящий HTTP-запрос; реализует `HTTP\Request`; `post()`, `query()`, `body()`, `isAjax()` |
|
||||||
@ -84,6 +85,7 @@ PhpStorm может показывать предупреждение «Namespac
|
|||||||
| `System\Classes\Session` | Синглтон сессии: `instance(string $name)`, `get/set/delete/destroy/regenerate` |
|
| `System\Classes\Session` | Синглтон сессии: `instance(string $name)`, `get/set/delete/destroy/regenerate` |
|
||||||
| `System\Classes\Config` | Статический конфиг: lazy-load `App/config/config.php` + merge `config.local.php` |
|
| `System\Classes\Config` | Статический конфиг: lazy-load `App/config/config.php` + merge `config.local.php` |
|
||||||
| `System\Classes\Validation` | Валидация входных данных по декларативным правилам; `factory()`, `rule()`, `check()`, `errors()` |
|
| `System\Classes\Validation` | Валидация входных данных по декларативным правилам; `factory()`, `rule()`, `check()`, `errors()` |
|
||||||
|
| `System\Classes\CSRF` | Защита форм от CSRF; токен в сессии: `token()`, `validate()`, `field()` |
|
||||||
| `System\Classes\HTTP` | `redirect()`, `requestHeaders()` |
|
| `System\Classes\HTTP` | `redirect()`, `requestHeaders()` |
|
||||||
| `System\Classes\HTTP\Header` | Extends `ArrayObject`; `send()`, `__toString()` → RFC-формат |
|
| `System\Classes\HTTP\Header` | Extends `ArrayObject`; `send()`, `__toString()` → RFC-формат |
|
||||||
| `System\Classes\HTTP\Message` | Интерфейс: `protocol()`, `headers()`, `body()`, `render()` |
|
| `System\Classes\HTTP\Message` | Интерфейс: `protocol()`, `headers()`, `body()`, `render()` |
|
||||||
@ -102,7 +104,7 @@ PhpStorm может показывать предупреждение «Namespac
|
|||||||
index.php
|
index.php
|
||||||
→ Core::init()
|
→ Core::init()
|
||||||
→ Request::factory() # detectUri(), Route::resolve(), заполняет post/query/body/method
|
→ Request::factory() # detectUri(), Route::resolve(), заполняет post/query/body/method
|
||||||
→ Request::execute() # require_once контроллера, вызывает {action}Action()
|
→ Request::execute() # require_once контроллера, вызывает executeAction({action}Action)
|
||||||
→ Response->body() # строка с HTML
|
→ Response->body() # строка с HTML
|
||||||
→ echo
|
→ echo
|
||||||
```
|
```
|
||||||
@ -206,6 +208,38 @@ if ($validation->check()) {
|
|||||||
|
|
||||||
Правила: `required`, `email`, `url`, `numeric`, `digit`, `min_length`, `max_length`, `exact_length`, `matches`, `in`, `regex`. Правила одного поля проверяются по порядку до первой ошибки. Пустое необязательное поле (нет `required`) остальные правила пропускает. Сообщения — шаблоны с плейсхолдерами `:field`/`:param1`/`:param2` (подстановка через `strtr`, как в `MyException`).
|
Правила: `required`, `email`, `url`, `numeric`, `digit`, `min_length`, `max_length`, `exact_length`, `matches`, `in`, `regex`. Правила одного поля проверяются по порядку до первой ошибки. Пустое необязательное поле (нет `required`) остальные правила пропускает. Сообщения — шаблоны с плейсхолдерами `:field`/`:param1`/`:param2` (подстановка через `strtr`, как в `MyException`).
|
||||||
|
|
||||||
|
### CSRF (System\Classes\CSRF)
|
||||||
|
|
||||||
|
Защита форм от CSRF. Токен хранится в сессии (`Session`), встраивается в форму скрытым полем и проверяется при обработке POST.
|
||||||
|
|
||||||
|
**Контроллеры — наследники `Controller` — проверяют токен автоматически** в `before()` на методах `POST/PUT/PATCH/DELETE` (отключается флагом `$_csrf_protection = false`, например для API/вебхуков). В шаблоне достаточно вставить поле:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use System\Classes\CSRF;
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<?= CSRF::field() ?>
|
||||||
|
...
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
Ручная проверка (если авто-CSRF отключён или нужен свой цикл) — через `CSRF::validate()`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use System\Classes\HTTP\HTTPException;
|
||||||
|
|
||||||
|
if (!CSRF::validate(Request::$current->post(CSRF::$key))) {
|
||||||
|
throw HTTPException::factory(403);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Контроллер создаётся в `Request::execute()` как `new $class()` без передачи запроса — доступ к данным через статический `Request::$current`, не через `$this->request`. Рабочий пример формы (валидация поверх авто-CSRF): `App/Controller/FeedbackController.php`.
|
||||||
|
|
||||||
|
- `token()` — токен текущей сессии (создаётся при первом обращении, `random_bytes(32)`).
|
||||||
|
- `validate($value)` — сравнение с сессией через `hash_equals()` (устойчиво к timing-атакам).
|
||||||
|
- `field()` — готовый `<input type="hidden">` с токеном.
|
||||||
|
- `CSRF::$key` — имя поля/ключа (по умолчанию `csrf_token`).
|
||||||
|
|
||||||
### HTTPException (System\Classes\HTTP\HTTPException)
|
### HTTPException (System\Classes\HTTP\HTTPException)
|
||||||
|
|
||||||
```php
|
```php
|
||||||
@ -240,6 +274,17 @@ $response = HTTPException::factory(403)->getResponse();
|
|||||||
- `$_paths` инициализируется лениво при первом вызове `findFile()`
|
- `$_paths` инициализируется лениво при первом вызове `findFile()`
|
||||||
- `Controller::render()` автоматически определяет `$dir` из имени класса (`App\Controller\FooController` → `view/Foo`)
|
- `Controller::render()` автоматически определяет `$dir` из имени класса (`App\Controller\FooController` → `view/Foo`)
|
||||||
|
|
||||||
|
### Controller hierarchy
|
||||||
|
|
||||||
|
Расстановка имён — как в проекте eoffice_v3:
|
||||||
|
|
||||||
|
- `System\Classes\BaseController` (abstract) — голое ядро: жизненный цикл `executeAction()` (`before()` → экшен → `after()`), хуки по умолчанию пустые. Нейтрально к вебу/API.
|
||||||
|
- `System\Classes\Controller extends BaseController` — веб: `render()` (layout + content), авто-CSRF в `before()`, `json($data, $status)` для JSON-ответов. **Контроллеры приложения наследуют его.**
|
||||||
|
|
||||||
|
API-контроллер делается не отдельным классом, а флагом: `extends Controller` + `$_csrf_protection = false` + ответы через `json()` (так же, как в eoffice_v3).
|
||||||
|
|
||||||
|
`Request::execute()` вызывает `executeAction({action}Action)`, поэтому `before()/after()` работают для любого контроллера прозрачно. Авторизацию добавлять в `before()` конкретных контроллеров (логина пока нет).
|
||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
Среда задаётся через `SetEnv APP_ENV` в `.htaccess` (PRODUCTION / STAGING / TESTING / DEVELOPMENT).
|
Среда задаётся через `SetEnv APP_ENV` в `.htaccess` (PRODUCTION / STAGING / TESTING / DEVELOPMENT).
|
||||||
@ -271,8 +316,10 @@ PHPUnit 11 в Docker-контейнере `bicycle`. Bootstrap: `tests/bootstrap
|
|||||||
| `tests/Unit/ConfigTest.php` | `get()`, `set()`, `merge()`, lazy-load |
|
| `tests/Unit/ConfigTest.php` | `get()`, `set()`, `merge()`, lazy-load |
|
||||||
| `tests/Unit/HTTPExceptionTest.php` | `factory()`, subclasses, `getResponse()`, codes |
|
| `tests/Unit/HTTPExceptionTest.php` | `factory()`, subclasses, `getResponse()`, codes |
|
||||||
| `tests/Unit/ValidationTest.php` | правила, `matches`, пропуск пустых, first-error, label/плейсхолдеры, fluent |
|
| `tests/Unit/ValidationTest.php` | правила, `matches`, пропуск пустых, first-error, label/плейсхолдеры, fluent |
|
||||||
|
| `tests/Unit/CSRFTest.php` | `token()` стабильность/формат, `validate()`, `field()`, нет токена в сессии |
|
||||||
|
| `tests/Unit/ControllerTest.php` | `executeAction()`, порядок `before/action/after`, no-op хуки |
|
||||||
|
|
||||||
**135 тестов, 215 assertion — все проходят.**
|
**145 тестов, 229 assertion — все проходят.**
|
||||||
|
|
||||||
### Frontend dependencies (через Composer)
|
### Frontend dependencies (через Composer)
|
||||||
|
|
||||||
|
|||||||
46
System/Classes/BaseController.php
Normal file
46
System/Classes/BaseController.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package Bicycle
|
||||||
|
* @author Egor Isaev
|
||||||
|
* @description BaseController.php
|
||||||
|
* @copyright (c) 23/06/2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace System\Classes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Голое ядро контроллера — жизненный цикл экшена: before() → {action} → after().
|
||||||
|
* Нейтрально к вебу/API. Наследник Controller добавляет рендеринг и политику.
|
||||||
|
*/
|
||||||
|
abstract class BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Хук перед экшеном. Переопределяется наследниками
|
||||||
|
* (проверка CSRF, авторизация и т.п.).
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function before(): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Хук после экшена.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function after(): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет экшен в обёртке before()/after().
|
||||||
|
*
|
||||||
|
* @param string $action Имя метода экшена (например 'indexAction')
|
||||||
|
* @return string Тело ответа
|
||||||
|
*/
|
||||||
|
public function executeAction(string $action): string
|
||||||
|
{
|
||||||
|
$this->before();
|
||||||
|
$body = $this->$action();
|
||||||
|
$this->after();
|
||||||
|
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
System/Classes/CSRF.php
Normal file
69
System/Classes/CSRF.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package Bicycle
|
||||||
|
* @author Egor Isaev
|
||||||
|
* @description CSRF.php
|
||||||
|
* @copyright (c) 23/06/2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace System\Classes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Защита форм от CSRF. Токен хранится в сессии, встраивается в форму
|
||||||
|
* скрытым полем и проверяется при обработке запроса.
|
||||||
|
*
|
||||||
|
* <form method="post">
|
||||||
|
* <?= CSRF::field() ?>
|
||||||
|
* ...
|
||||||
|
* </form>
|
||||||
|
*
|
||||||
|
* if (!CSRF::validate($request->post('csrf_token'))) {
|
||||||
|
* throw HTTPException::factory(403);
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
class CSRF
|
||||||
|
{
|
||||||
|
/** @var string Имя поля/ключа сессии для токена */
|
||||||
|
public static string $key = 'csrf_token';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает токен текущей сессии, создавая его при первом обращении.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function token(): string
|
||||||
|
{
|
||||||
|
$session = Session::instance();
|
||||||
|
$token = $session->get(self::$key);
|
||||||
|
|
||||||
|
if (!$token) {
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$session->set(self::$key, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет присланный токен против токена из сессии.
|
||||||
|
*
|
||||||
|
* @param string|null $value Значение из Request::post(CSRF::$key)
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function validate(?string $value): bool
|
||||||
|
{
|
||||||
|
$token = Session::instance()->get(self::$key);
|
||||||
|
|
||||||
|
return is_string($value) && is_string($token) && hash_equals($token, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает готовое скрытое поле с токеном для вставки в форму.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function field(): string
|
||||||
|
{
|
||||||
|
return '<input type="hidden" name="' . self::$key . '" value="' . self::token() . '">';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,14 +8,43 @@
|
|||||||
|
|
||||||
namespace System\Classes;
|
namespace System\Classes;
|
||||||
|
|
||||||
|
use System\Classes\HTTP\HTTPException;
|
||||||
|
use System\Classes\HTTP\Request as HTTPRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Базовый контроллер. render() строит двухуровневый вывод: layout + content.
|
* Веб-контроллер: рендеринг layout + content, авто-проверка CSRF на небезопасных
|
||||||
|
* методах и JSON-ответы. Контроллеры приложения наследуют его.
|
||||||
*/
|
*/
|
||||||
class Controller
|
class Controller extends BaseController
|
||||||
{
|
{
|
||||||
/** @var string Имя layout-шаблона в System/view/views */
|
/** @var string Имя layout-шаблона в System/view/views */
|
||||||
protected string $_layout = 'layout';
|
protected string $_layout = 'layout';
|
||||||
|
|
||||||
|
/** @var bool Проверять ли CSRF-токен на небезопасных методах */
|
||||||
|
protected bool $_csrf_protection = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет CSRF-токен на POST/PUT/PATCH/DELETE.
|
||||||
|
* Отключается флагом $_csrf_protection (например, для API/вебхуков).
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws HTTPException 403, если токен не прошёл
|
||||||
|
*/
|
||||||
|
protected function before(): void
|
||||||
|
{
|
||||||
|
if (!$this->_csrf_protection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = Request::$current;
|
||||||
|
$method = $request?->method() ?? HTTPRequest::GET;
|
||||||
|
$unsafe = [HTTPRequest::POST, HTTPRequest::PUT, HTTPRequest::PATCH, HTTPRequest::DELETE];
|
||||||
|
|
||||||
|
if (in_array($method, $unsafe, true) && !CSRF::validate($request->post(CSRF::$key))) {
|
||||||
|
throw HTTPException::factory(403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Рендерит шаблон контента внутри layout.
|
* Рендерит шаблон контента внутри layout.
|
||||||
* Если $dir пуст — определяется автоматически из имени класса
|
* Если $dir пуст — определяется автоматически из имени класса
|
||||||
@ -30,12 +59,29 @@ class Controller
|
|||||||
protected function render(string $template, array $data = [], string $dir = ''): string
|
protected function render(string $template, array $data = [], string $dir = ''): string
|
||||||
{
|
{
|
||||||
if ($dir === '') {
|
if ($dir === '') {
|
||||||
$class = substr(get_class($this), strlen('App\\Controller\\')); // [Admin\]FooController
|
$class = substr(get_class($this), strlen('App\\Controller\\')); // [Admin\]FooController
|
||||||
$dir = 'view/' . str_replace('\\', '/', substr($class, 0, -10)); // view/[Admin/]Foo
|
$dir = 'view/' . str_replace('\\', '/', substr($class, 0, -10)); // view/[Admin/]Foo
|
||||||
}
|
}
|
||||||
|
|
||||||
return (new View($this->_layout, 'view/views', [
|
return (new View($this->_layout, 'view/views', [
|
||||||
'content' => (new View($template, $dir, $data))->render()
|
'content' => (new View($template, $dir, $data))->render()
|
||||||
]))->render();
|
]))->render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Формирует JSON-ответ: ставит статус и Content-Type, кодирует данные.
|
||||||
|
*
|
||||||
|
* @param mixed $data Данные ответа
|
||||||
|
* @param int $status HTTP-статус
|
||||||
|
* @return string JSON
|
||||||
|
*/
|
||||||
|
protected function json(mixed $data, int $status = 200): string
|
||||||
|
{
|
||||||
|
http_response_code($status);
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -129,7 +129,7 @@ class Request implements HTTPRequest
|
|||||||
return HTTPException::factory(404)->getResponse();
|
return HTTPException::factory(404)->getResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (new Response())->body((new $class())->$method());
|
return (new Response())->body((new $class())->executeAction($method));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user