Попытка написать свой роутер на базе 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;
}
AppRouter::init(
logger: AppLogger::scope('routing'),
/* other options */
);
Опции:
namespace
- неймспейс по-умолчанию, может быть задан вызовомAppRouter::setDefaultNamespace()
prefix
- префикс URL (аналогично поведению для групп)allowEmptyHandlers
(false) - разрешить ли пустые хэндлеры?allowEmptyGroups
(false) - разрешить ли пустые группы?
Некоторые опции могут быть переопределены только вызовом:
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)
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")
Класс может выкинуть три исключения:
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'
Вызовет исключение:
BadRouteException: Cannot register two routes matching "/user/([^/]+)" for method "GET""
Происходит это, очевидно, потому что без алиасов подгруппы {userid}
и {username}
раскрываются в ([^/]+)
, а роуты с
одинаковыми URL определить нельзя.
NB:
В версии 2.0.* реализованы только глобальные алиасы. Возможности задать алиасы для роутов группы (и только для них) нет.
- Опция
middlewareNamespace
дляinit()
- неймспейс посредников по умолчанию.