Попытка написать свой роутер на базе 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.* реализованы только глобальные алиасы. Возможности задать алиасы для роутов группы (и только для них) нет.
echo AppRouter\Helper::dumpRoutingRulesWeb( AppRouter::getRoutingRules(), false );die;Даст примерно такую картинку:
- Опция
middlewareNamespaceдляinit()- неймспейс посредников по умолчанию.