gzhegow / router
There is no license information available for the latest version (1.0.11) of this package.
1.0.11
2024-08-28 12:31 UTC
Requires
- php: ^7.2|^8.0
README
Маршрутизатор с паттернами, построением URL, привязкой Middleware/Fallback как к самим маршрутам, так и к путям (если маршрут не найден).
Маршрутизация происходит через вложенное дерево маршрутов, а не через прямой обход всех маршрутов, то есть выполняется минимальное число регулярных выражений.
Поддерживает кеширование, можно использовать symfony/cache
или сохранять в файл.
Установка
composer require gzhegow/router;
Пример
<?php use Gzhegow\Router\Lib; use Gzhegow\Router\Router; use Gzhegow\Router\RouterFactory; use Gzhegow\Router\RouterInterface; use Gzhegow\Router\Cache\RouterCache; use Gzhegow\Router\Contract\RouterMatchContract; use Gzhegow\Router\Contract\RouterDispatchContract; use Gzhegow\Router\Handler\Middleware\CorsMiddleware; use Gzhegow\Router\Handler\Demo\Fallback\DemoFallback; use Gzhegow\Router\Handler\Demo\Controller\DemoController; use Gzhegow\Router\Handler\Demo\Fallback\DemoLogicFallback; use Gzhegow\Router\Handler\Demo\Middleware\Demo1stMiddleware; use Gzhegow\Router\Handler\Demo\Middleware\Demo2ndMiddleware; use Gzhegow\Router\Handler\Demo\Fallback\DemoRuntimeFallback; require_once __DIR__ . '/vendor/autoload.php'; // > настраиваем PHP ini_set('memory_limit', '32M'); // > настраиваем обработку ошибок error_reporting(E_ALL); set_error_handler(function ($errno, $errstr, $errfile, $errline) { if (error_reporting() & $errno) { throw new \ErrorException($errstr, -1, $errno, $errfile, $errline); } }); set_exception_handler(function ($e) { var_dump(Lib::php_dump($e)); var_dump($e->getMessage()); var_dump(($e->getFile() ?? '{file}') . ': ' . ($e->getLine() ?? '{line}')); die(); }); // > создаем роутер $router = (new RouterFactory())->newRouter(); // >>> Ставим настройки роутера $settings = [ // > позволить регистрировать callable с объектами и \Closure, кеш в этом случае работать не будет $registerAllowObjectsAndClosures = false, // // > на этапе добавления проверить маршрут на предмет того, заканчивается ли он на слеш и бросить исключение $compileTrailingSlashMode = Router::TRAILING_SLASH_AS_IS, // Router::TRAILING_SLASH_AS_IS // > оставить как есть // Router::TRAILING_SLASH_ALWAYS, // > при вызове маршрута добавлять к нему trailing-slash // Router::TRAILING_SLASH_NEVER, // > при вызове маршрута удалить trailing-slash // // > не учитывать метод при вызове dispatch(), в этом случае POST действия будут отрабатывать даже на запросы из браузера (однако порядок регистрации важен, если на том же маршруте GET/POST, то отработает тот что раньше зарегистрирован) $dispatchIgnoreMethod = false, // // > подменить метод при вызове dispatch(), например, несмотря на GET запрос выполнить действие, зарегистрированное на POST $dispatchForceMethod = null, // HttpMethod::METHOD_POST | HttpMethod::METHOD_GET | HttpMethod::METHOD_PUT | HttpMethod::METHOD_OPTIONS | etc. // // > при вызове dispatch() к переданному маршруту добавить обратный слеш, удалить его или оставить без изменений $dispatchTrailingSlashMode = Router::TRAILING_SLASH_AS_IS, // TRAILING_SLASH_ALWAYS | TRAILING_SLASH_NEVER // Router::TRAILING_SLASH_AS_IS // > оставить как есть // Router::TRAILING_SLASH_ALWAYS, // > при вызове маршрута добавлять к нему trailing-slash // Router::TRAILING_SLASH_NEVER, // > при вызове маршрута удалить trailing-slash ]; $router->setSettings(...$settings); // >>> Настраиваем кеш для роутера $cacheDir = __DIR__ . '/var/cache'; $cacheNamespace = 'app.router'; // >>> Можно использовать путь к файлу, в этом случае кеш будет сделан через file_{get|put}_contents() + (un)serialize() $cacheDirpath = "{$cacheDir}/{$cacheNamespace}"; $cacheFilename = "router.cache"; $router->setCacheSettings([ // 'cacheMode' => Reflector::CACHE_MODE_NO_CACHE, // > не использовать кеш совсем 'cacheMode' => RouterCache::CACHE_MODE_STORAGE, // > использовать файловую систему или адаптер (хранилище) // 'cacheDirpath' => $cacheDirpath, 'cacheFilename' => $cacheFilename, ]); // >>> Либо можно установить пакет `composer require symfony/cache` и использовать адаптер, чтобы запихивать в Редис например // $symfonyCacheAdapter = new \Symfony\Component\Cache\Adapter\FilesystemAdapter( // $cacheNamespace, $defaultLifetime = 0, $cacheDir // ); // $redisClient = \Symfony\Component\Cache\Adapter\RedisAdapter::createConnection('redis://localhost'); // $symfonyCacheAdapter = new \Symfony\Component\Cache\Adapter\RedisAdapter( // $redisClient, // $cacheNamespace = '', // $defaultLifetime = 0 // ); // $router->setCacheSettings([ // 'cacheMode' => Reflector::CACHE_MODE_STORAGE, // 'cacheAdapter' => $symfonyCacheAdapter, // ]); // > вызываем функцию, которая загрузит кеш, и если его нет - выполнит регистрацию маршрутов $router->cacheRemember(function (RouterInterface $router) { // > добавляем паттерн, который можно использовать в маршрутах $router->pattern('{id}', '[0-9]+'); // > добавляет Middleware по пути (они отработают даже если маршрут не найден, но путь начинался с указанного) $router->middlewareOnPath('/api/v1/user', CorsMiddleware::class); $router->middlewareOnPath('/api/v1/user', Demo1stMiddleware::class); $router->middlewareOnPath('/api/v1/user', Demo2ndMiddleware::class); // > добавляет Fallback по пути (если во время действия будет брошено исключение или роута не будет - запустится это действие) // > несколько Fallback запустятся один за другим, пока какой-либо из них не вернет результат, если результата так и не будет - исключение будет брошено снова $router->fallbackOnPath('/api/v1/user', DemoFallback::class); // > к маршрутам можно привязывать теги, на теги подключать Middleware и Fallback, также по тегам можно искать маршруты // $router->middlewareOnTag('user', DemoMiddleware::class); // $router->fallbackOnTag('user', DemoFallback::class); // > для того, чтобы зарегистрировать маршруты удобно использовать группировку // $router->group()->middlewareList([]); // ->middlewareList([]) // > использование метода, который заканчивается на `List` перезапишет предыдущие // $router->group()->tagList([]); // ->tagList([]) // > использование метода, который заканчивается на `List` перезапишет предыдущие $router->group() ->tag('user') // > ставим тег для каждого роута в группе ->middleware([ CorsMiddleware::class ]) // > подключаем CORS (в примере сделано "разрешить всё", если нужны тонкие настройки - наследуйте класс `CorsMiddleware` или напишите новый) ->middleware([ // > подключаем другие Middleware Demo1stMiddleware::class, Demo2ndMiddleware::class, ]) ->fallback([ DemoFallback::class, // > этот Fallback ранее уже был зарегистрирован по пути, на этапе вызова они совпадут и вызовется один раз DemoLogicFallback::class, // > этот Fallback написан обрабатывать только \LogicException DemoRuntimeFallback::class, // > этот Fallback написан обрабатывать только \RuntimeException ]) ->register(function (RouterInterface $router) { $router->route('/api/v1/user/{id}/main', 'GET', [ DemoController::class, 'mainGet' ], 'user.main'); $router->route('/api/v1/user/{id}/main', 'POST', [ DemoController::class, 'mainPost' ], 'user.main'); // > это имя мы уже использовали выше, однако path совпадает и так можно // > В принципе, обработку Cors можно подключить и через CorsAction, но без Middleware всё равно не обойдется, т.к. заголовки отправляются и в каждом запросе и в методе OPTIONS - но там их больше // $router->route('/api/v1/user/{id}/main', 'OPTIONS', CorsAction::class, 'user.main'); $router->route('/api/v1/user/{id}/logic', 'GET', $action = [ DemoController::class, 'logic' ], $name = 'user.logic'); $router->route('/api/v1/user/{id}/runtime', 'GET', $action = [ DemoController::class, 'runtime' ]) ->middleware([ // > эти Middleware уже были заданы на группе, на этапе вызова они совпадут и вызовутся один раз CorsMiddleware::class, Demo1stMiddleware::class, Demo2ndMiddleware::class, ]) ->fallback([ // > эти Fallback уже были заданы на группе, на этапе вызова они совпадут и вызовутся один раз DemoFallback::class, DemoLogicFallback::class, DemoRuntimeFallback::class, ]) ; }) ; // > однако так тоже можно // $router->routeAdd( // $router->blueprint() // ->path('/api/v1/user/{id}/main') // ->httpMethod('GET') // ->action([ DemoController::class, 'main' ]) // ); }); // > так можно искать маршруты с помощью имен или тегов echo 'Case 1:' . PHP_EOL; // > все результаты // [ 1 => $routes1, 2 => $routes2 ] = $batch = $router->matchAllByNames([ 1 => 'user.main', 2 => 'user.logic' ]); // [ 1 => $routes1, 2 => $routes2 ] = $batch = $router->matchAllByTags([ 1 => 'tag1', 2 => 'tag2' ]); // > первый результат // $route = $router->matchFirstByName('user.main'); // $route = $router->matchFirstByTag('user'); $batch = $router->matchAllByNames([ 'user.main' ]); foreach ( $batch as $id => $routes ) { var_dump([ $id, array_map([ Lib::class, 'php_dump' ], $routes), ]); } // array(2) { // [0]=> // int(0) // [1]=> // array(2) { // [0]=> // string(43) "{ object(Gzhegow\Router\Route\Route # 51) }" // [1]=> // string(43) "{ object(Gzhegow\Router\Route\Route # 56) }" // } // } echo PHP_EOL; // > так можно искать маршруты с помощью нескольких фильтров (если указать массивы - они работают как логическое ИЛИ, тогда как сами фильтры работают через логическое И echo 'Case 2:' . PHP_EOL; $contract = RouterMatchContract::from([ // 'id' => 1, // 'ids' => [ 1 ], // 'path' => '/api/v1/user/{id}', // 'pathes' => [ '/api/v1/user/{id}' ], // 'httpMethod' => 'GET', // 'httpMethods' => [ 'GET' ], // 'name' => 'user.main', // 'names' => [ 'user.main' ], // 'tag' => 'user', // 'tags' => [ 'user' ], // 'name' => 'user.main', 'tag' => 'user', 'httpMethods' => [ 'GET', 'POST' ], ]); $routes = $router->matchByContract($contract); foreach ( $routes as $id => $route ) { var_dump([ $id, Lib::php_dump($route) ]); } // array(2) { // [0]=> // int(0) // [1]=> // string(43) "{ object(Gzhegow\Router\Route\Route # 51) }" // } // array(2) { // [0]=> // int(1) // [1]=> // string(43) "{ object(Gzhegow\Router\Route\Route # 56) }" // } echo PHP_EOL; // > так можно запустить выполнение маршрута в вашем файле index.php, на который указывает apache2/nginx echo 'Case 3:' . PHP_EOL; $contract = RouterDispatchContract::from([ 'GET', '/api/v1/user/1/main' ]); $result = $router->dispatch($contract); var_dump($result); // @before :: Gzhegow\Router\Handler\Demo\Middleware\Demo1stMiddleware::__invoke // @before :: Gzhegow\Router\Handler\Demo\Middleware\Demo2ndMiddleware::__invoke // string(59) "Gzhegow\Router\Handler\Demo\Controller\DemoController::main" // @after :: Gzhegow\Router\Handler\Demo\Middleware\Demo2ndMiddleware::__invoke // @after :: Gzhegow\Router\Handler\Demo\Middleware\Demo1stMiddleware::__invoke // int(1) echo PHP_EOL; // > вот такого маршрута нет, запустится ранее указанный fallback, однако Fallback возвращает NULL, поэтому исключение все равно будет выброшено ещё раз echo 'Case 4:' . PHP_EOL; $contract = RouterDispatchContract::from([ 'GET', '/api/v1/user/not-found' ]); $result = null; try { $result = $router->dispatch($contract); } catch ( \Throwable $e ) { var_dump('CATCH: ' . get_class($e)); } var_dump($result); // string(59) "Gzhegow\Router\Handler\Demo\Fallback\DemoFallback::__invoke" // string(57) "CATCH: Gzhegow\Router\Exception\Runtime\NotFoundException" // NULL echo PHP_EOL; // > этот маршрут бросает \LogicException, запустятся DemoFallback и DemoLogicFallback echo 'Case 5:' . PHP_EOL; $contract = RouterDispatchContract::from([ 'GET', '/api/v1/user/1/logic' ]); $result = $router->dispatch($contract); var_dump($result); // @before :: Gzhegow\Router\Handler\Demo\Middleware\Demo1stMiddleware::__invoke // @before :: Gzhegow\Router\Handler\Demo\Middleware\Demo2ndMiddleware::__invoke // string(60) "Gzhegow\Router\Handler\Demo\Controller\DemoController::logic" // string(59) "Gzhegow\Router\Handler\Demo\Fallback\DemoFallback::__invoke" // string(64) "Gzhegow\Router\Handler\Demo\Fallback\DemoLogicFallback::__invoke" // @after :: Gzhegow\Router\Handler\Demo\Middleware\Demo2ndMiddleware::__invoke // @after :: Gzhegow\Router\Handler\Demo\Middleware\Demo1stMiddleware::__invoke // bool(true) echo PHP_EOL; // > этот маршрут бросает \RuntimeException, запустятся DemoFallback и DemoRuntimeFallback echo 'Case 6:' . PHP_EOL; $contract = RouterDispatchContract::from([ 'GET', '/api/v1/user/1/runtime' ]); $result = $router->dispatch($contract); var_dump($result); // @before :: Gzhegow\Router\Handler\Demo\Middleware\Demo1stMiddleware::__invoke // @before :: Gzhegow\Router\Handler\Demo\Middleware\Demo2ndMiddleware::__invoke // string(62) "Gzhegow\Router\Handler\Demo\Controller\DemoController::runtime" // string(59) "Gzhegow\Router\Handler\Demo\Fallback\DemoFallback::__invoke" // string(66) "Gzhegow\Router\Handler\Demo\Fallback\DemoRuntimeFallback::__invoke" // @after :: Gzhegow\Router\Handler\Demo\Middleware\Demo2ndMiddleware::__invoke // @after :: Gzhegow\Router\Handler\Demo\Middleware\Demo1stMiddleware::__invoke // bool(true) echo PHP_EOL; // > так можно сгенерировать ссылки для зарегистрированных маршрутов по именам echo 'Case 7:' . PHP_EOL; $instances = []; $instances[ 'a' ] = $router->matchFirstByName('user.main'); // $names = []; $names[ 'b' ] = 'user.main'; $names[ 'c' ] = 'user.main'; // $routes = $instances + $names; // $attributes = []; $ids = []; $ids[ 'a' ] = 1; $ids[ 'b' ] = 2; $ids[ 'c' ] = 3; // $attributes = [ 'id' => $ids ]; // > можно передать либо список объектов (instance of Route::class) и/или список строк (route `name`) $result = $router->urls($routes, $attributes); var_dump($result); // array(3) { // ["a"]=> // string(19) "/api/v1/user/1/main" // ["b"]=> // string(19) "/api/v1/user/2/main" // ["c"]=> // string(19) "/api/v1/user/3/main" // }