karelwintersky / arris.router
Arris Application µFramework - AppRouter class
Requires
- php: 8.*
- psr/log: *
Requires (Dev)
Suggests
- karelwintersky/arris: Arris µFramework core
- karelwintersky/arris.helpers: Arris µFramework helpers
- karelwintersky/arris.logger: Arris µFramework AppLogger
README
Попытка написать свой роутер на базе https://github.com/nikic/FastRoute
Реализует возможность статического класса AppRouter.
init()
get()
post()
- etc
Пример использования:
use Arris\AppRouter; use Arris\Exceptions\{ AppRouterHandlerError, AppRouterMethodNotAllowedException, AppRouterNotFoundException }; try { AppRouter::init( logger: null, allowEmptyHandlers: true, ); AppRouter::get('/', [ DynamicClass::class, 'present_dynamic_method'], 'root'); AppRouter::get('/function/', 'example_function', 'root.function_call'); AppRouter::group( prefix: '/admin', before: 'MiddleAdmin@before', after: [ MiddleAdmin::class, 'after' ], callback: function () { AppRouter::get('/', function () { d('this is simple closure'); }, 'admin.root'); AppRouter::get('/foo[/]', 'StaticClass@present_static_method', 'admin.foo'); AppRouter::get('/list/', [StaticClass::class, 'present_static_method'], 'admin.list'); AppRouter::group( prefix: '/users', before: [MiddleAdminUsers::class, 'before'], after: [MiddleAdminUsers::class, 'after'], callback: static function() { AppRouter::get('/', [ DynamicClass::class, 'users'], 'admin.users.root'); AppRouter::get('/all/', 'DynamicClass@all', 'admin.users.all'); AppRouter::get('/invoke/', 'DynamicClass@' , 'admin.users.invoke'); AppRouter::get('/list/', [StaticClass::class, 'method_not_exist'], 'admin.users.list'); AppRouter::get('/empty/[{id:\d+}[/]]', /*[ DynamicClass::class, 'create']*/ [] , 'admin.users.empty'); } ); } ); AppRouter::dispatch(); } catch (AppRouterHandlerError|AppRouterNotFoundException|AppRouterMethodNotAllowedException $e) { var_dump($e->getMessage()); } catch (RuntimeException|Exception $e) { var_dump($e); echo "<br>" . PHP_EOL; }
Детали
init - Инициализация роутера
AppRouter::init( logger: AppLogger::scope('routing'), /* other options */ );
Опции:
namespace
- неймспейс по-умолчанию, может быть задан вызовомAppRouter::setDefaultNamespace()
prefix
- префикс URL (аналогично поведению для групп)allowEmptyHandlers
(false) - разрешить ли пустые хэндлеры?allowEmptyGroups
(false) - разрешить ли пустые группы?
setOption - переопределение опций
Некоторые опции могут быть переопределены только вызовом:
AppRouter::setOption(name, value);
Допустимые имена опций:
AppRouter::OPTION_ALLOW_EMPTY_HANDLERS
- разрешить пустые (заданные как[]
) хэндлеры? Если false - кидается исключениеAppRouterHandlerError: Handler not found or empty
.AppRouter::OPTION_ALLOW_EMPTY_GROUPS
- разрешить ли пустые группы? Пустой считается группа без роутов. Если разрешено - для такой группы будут парситься миддлвары и опции.AppRouter::OPTION_DEFAULT_ROUTE
- дефолтное значение для реверс-роутингаAppRouter::OPTION_USE_ALIASES
- разрешить ли алиасы?
Декларация роутов
Методы: get
, post
, put
, patch
, delete
, head
, options
AppRouter::method( route: '/my/awesome/uri/', handler: хэндлер, name: 'имя' );
route
- строка (с регулярками/алиасами регулярок)handler
- хэндлерname
- имя роута для обратного роутинга (reverse routing)
Как можно задать handler?
function() { }
, то есть Closure;[Class::class, 'method']
- массив из двух элементов, подразумевается, что метод динамический, то есть класс будет инстанциирован перед вызовом метода.Class@method
- строка, содержащая@
. Будет применена рефлексия для вычисления типа метода. Если метод динамический - класс будет инстанциирован.Class@
- будет вызван метод__invoke()
у класса.function
- функцияnull
- строго пустой роут, вызов всегда выбросит исключениеAppRouterNotFoundException -> URL not found
[]
. По умолчанию будет выброшено исключениеAppRouterHandlerError
, но... есть нюанс:
Пустой хэндлер?
Если задать опцию allowEmptyHandlers: true
или вызвать AppRouter::setOption('allowEmptyHandlers', true)
, то можно
будет использовать пустые хэндлеры, например:
AppRouter::get('/admin/users/', [], 'admin.users.root');
В этом случае пройдет стандартная цепочка роутинга - будут инстанциированы и вызваны миддлвары, сначала before, потом в обратном порядке after, например:
string(30) "Class MiddleAdmin instantiated"
string(19) "MiddleAdmin::before"
string(35) "Class MiddleAdminUsers instantiated"
string(24) "MiddleAdminUsers::before"
<тут должен был обрабатываться хэндлер, но он пуст>
string(23) "MiddleAdminUsers::after"
string(18) "MiddleAdmin::after"
Группировка роутов
\Arris\AppRouter::group( prefix: '/admin', before: 'MiddleAdmin@before', after: [ MiddleAdmin::class, 'after' ], callback: function () { /* роуты группы */ } );
Реверс-роутинг
AppRouter::getRouter(name)
возвращает URL, соответствующий имени роута.
При этом, имя *
вернет все маршруты. Если имя не найдено - будет возвращен роут по-умолчанию.
При этом:
- именованные группы-плейсхолдеры будут заменены на переданные переменные
- необязательные оконечные слэши будут заменены на обязательные
- будут удалены необязательные группы
Таким образом, если роут определен:
AppRouter::get('/entry/delete/{id}/', 'handler', 'callback_entry_delete');
То вызов
Arris\AppRouter::getRouter('callback_entry_delete', [ 'id' => 15 ])
Сгенерирует строчку: /entry/delete/15/
Роут по-умолчанию
Если роут не найден или передан пустой роут - будет возвращен URL /
. Это поведение может быть переопределено вызовом:
AppRouter::setOption('getRouterDefaultValue', '/foo/bar');
Вызов реверс-роутинга в шаблонах
Что полезно, реверс-роутинг может вызываться в Smarty-шаблонах:
<button data-url="{Arris\AppRouter::getRouter('callback_entry_delete', [ 'id' => $item.user_id ])}">Delete Entry</button>
Что требует определения в Smarty или Arris.Presenter:
->registerClass("Arris\AppRouter", "Arris\AppRouter")
Исключения (Exceptions)
Класс может выкинуть три исключения:
AppRouterHandlerError
- ошибка в хэндлере (пустой, неправильный, итп)AppRouterNotFoundException
- роут не определен (URL ... not found)AppRouterMethodNotAllowedException
- используемый метод недопустим для этого роута
При этом передается расширенная информация по роуту, получить которую можно через метод $e->getError()
, потому что
переопределить финальный метод getMessage()
НЕВОЗМОЖНО.
ЭКСПЕРИМЕНТАЛЬНАЯ ФИЧА: АЛИАСЫ
Включается с помощью
AppRouter::setOption('useAliases', true);
После этого можно задать алиасы:
AppRouter::addAlias([ [ 'userid' => '\d+' ], [ 'username' => '[a-zA-Z]+' ] ]);
И определить роуты:
AppRouter::get( route: '/user/{userid}[/]', handler: function ($userid = 0) { var_dump('Closure => userid: ' . $userid ); }, name: 'root.userid' ); AppRouter::get( route: '/user/{username}[/]', handler: function ($username = 'anon') { var_dump('Closure => username: ' . $username ); }, name: 'root.username' );
Теперь при вызове /user/<value>/
в зависимости от совпадения с регуляркой будет вызван один из хэндлеров:
\d+
, то есть число - хэндлер userid[a-zA-Z]+
, то есть латинская строка - хэндлер username
Реверс-роутинг и алиасы
Прекрасно работает:
echo AppRouter::getRouter('root.userid', [ 'userid' => 42 ]); // => /user/42/ echo AppRouter::getRouter('root.username', [ 'username' => 'wombat' ]); // => /user/wombat/
Опциональные группы и алиасы
В данном случае объявить опциональной можно только одну группу, хотя так делать не стоит:
AppRouter::get('/user/{userid}[/]', function ($userid = 0) { var_dump('Closure => userid: ' . $userid ); }); AppRouter::get('/user/[{username}[/]]', function ($username = 'anon') { var_dump('Closure => username: ' . $username ); });
При переходе на /user/
произойдет вызов хэндлера username.
Объявление двух групп опциональными вызовет исключение:
BadRouteException: Cannot register two routes matching "/user/" for method "GET"
Как на самом деле сделать "опциональную" группу:
AppRouter::get('/user/', function () { var_dump('Closure => user root ' ); }); AppRouter::get('/user/{userid}[/]', function ($userid = 0) { var_dump('Closure => userid: ' . $userid ); }); AppRouter::get('/user/{username}[/]', function ($username = 'anon') { var_dump('Closure => username: ' . $username ); });
/user
= 'Closure => user root'/user/123/
= 'Closure => userid: 123'/username/wombat/
= 'Closure => username: wombat'
Использование алиасов с выключенной опцией useAliases
Вызовет исключение:
BadRouteException: Cannot register two routes matching "/user/([^/]+)" for method "GET""
Происходит это, очевидно, потому что без алиасов подгруппы {userid}
и {username}
раскрываются в ([^/]+)
, а роуты с
одинаковыми URL определить нельзя.
NB:
В версии 2.0.* реализованы только глобальные алиасы. Возможности задать алиасы для роутов группы (и только для них) нет.
ToDo
- Опция
middlewareNamespace
дляinit()
- неймспейс посредников по умолчанию.