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

This package is auto-updated.

Last update: 2024-10-28 12:55:09 UTC


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"
// }