rguezque/katya-router

轻量级PHP路由器。

1.1.0 2024-08-21 18:53 UTC

This package is auto-updated.

Last update: 2024-10-01 05:52:22 UTC


README

轻量级PHP路由器

目录表

安装

在项目根目录下的终端

composer require rguezque/katya-router

配置

对于 Apache 服务器,在项目目录中创建并编辑一个 .htaccess 文件,内容如下

<IfModule mod_rewrite.c>
    RewriteEngine On

    # Handle Authorization Header
    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    # Redirect Trailing Slashes If Not A Folder...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} (.+)/$
    RewriteRule ^ %1 [L,R=301]

    # Handle Front Controller...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

对于 Nginx,按照以下方式编辑配置文件

server {
    location / {
        try_files $uri $uri/ /index.php;
    }
}

对于使用PHP内置服务器进行测试,在项目目录中,在终端执行

php -S localhost:80

然后在网页浏览器中打开地址 https://:80

自动加载

在终端中,定位到项目目录并执行

composer dump-autoload -o

路由

require __DIR__.'/vendor/autoload.php';

use rguezque\{
    HttpStatus,
    Katya, 
    Request,
    Response
};
use rguezque\Exceptions\{
    RouteNotFoundException, 
    UnsupportedRequestMethodException
};

$router = new Katya;

$router->route(Katya::GET, '/', function(Request $request, Response $response) {
    $response->send('hola mundo!');
});

try {
    $router->run(Request::fromGlobals());
} catch(RouteNotFoundException $e) {
    $message = sprintf('<h1>Not Found</h1><p>%s</p>', $e->getMessage());
    (new Response($message, HttpStatus::HTTP_NOT_FOUND))->send();
} catch(UnsupportedRequestMethodException $e) {
    $message = sprintf('<h1>Not Allowed</h1><p>%s</p>', $e->getMessage());
    (new Response($message, HttpStatus::HTTP_METHOD_NOT_ALLOWED))->send();
} 

每个路由都通过 Katya::route 方法定义,该方法接收3个参数:请求方法(仅支持 GETPOSTPUTPATCHDELETE)、路由和对应路由的控制器。控制器始终接收2个参数:一个 Request 对象(查看 Request)和一个 Response 对象(查看 Response)。第一个包含处理请求所需的方法,第二个包含允许返回响应的方法。

要启动路由器,请调用 Katya::run 方法,并传递一个 Request 对象。

如果路由器位于子目录中,可以在创建路由器实例时在选项数组中指定它。同样,可以定义渲染模板时默认搜索的目录。

$katya = new Katya([
    'basepath' => '/nombre_directorio_base',
    'viewspath' => __DIR__.'/templates/'
]);

提示

路由器返回两种可能的异常;当找不到路由时返回 RouteNotFoundException,当请求方法不受路由器支持时返回 UnsupportedRequestMethodException。使用 try-catch 来捕获它们,并像示例中那样处理适当的 Response

快捷键

快捷方式 Katya::getKatya::postKatya::putKatya::patchKatya::delete 分别用于向路由器添加类型为 GETPOSTPUTPATCHDELETE 的路由。

$katya = new Katya;
$katya->get('/', function(Request $request, Response $response) {
    $response->send('Hello')
});

$katya->post('/', function(Request $request, Response $response) {
    $data = [
        'name' => 'John',
        'lastname' => 'Doe'
    ];

    $response->json($data);
});

控制器

控制器可以是:匿名函数、静态方法或对象方法。

// Usando una función anónima
$katya->get('/user', function(Request $request, Response $response) {
    //...
});

// Usando un método estático
$katya->get('/user', ['App\Controller\Greeting', 'showProfile']);
// o bien
use App\Controller\User;
$katya->get('/user', [User::class, 'showProfile']);
$katya->get('/user/permissions', [User::class, 'showPermissions']);

// Usando un método de un objeto
$user = new App\Controller\User();
$katya->get('/user', [$user, 'showProfile']);

路由分组

要创建具有相同前缀的路由分组,请使用 Katya::group;它接受2个参数:路由前缀和一个接收包含组路由定义的 Group 对象的匿名函数。

// Se generan las rutas "/foo/bar" y "/foo/baz"
$katya->group('/foo', function(Group $group) {
    $group->get('/bar', function(Request $request, Response $response) {
        $response->send(' Hello foobar');
    });

    $group->get('/baz', function(Request $request, Response $response) {
        $response->render('welcome.php')
    });
});

通配符

通配符是路由中定义的参数。路由器根据请求查找匹配项,并将它们作为参数发送到通过 Request 对象传递的路由控制器,这些参数可以通过 Request::getParams 方法检索,该方法返回一个 Parameters 对象,其中每个键都与通配符的名称相对应。

$katya->get('/hola/{nombre}', function(Request $request, Response $response) {
    $params = $request->getParams();
    $response->send(sprintf('Hola %s', $params->get('nombre')));
});

如果将 通配符 定义为正则表达式,则可以使用 Request::getMatches 方法进行检索,该方法返回一个包含找到的匹配项值的 线性数组

$katya->get('/hola/(\w+)/(\w+)', function(Request $request, Response $response) {
    $params = $request->getMatches();
    list($nombre, $apellido) = $params;
    $response->send(sprintf('Hola %s %s', $nombre, $apellido));
});

重要

请避免在同一个路由定义中混合命名参数和正则表达式,因为您将无法按名称检索被定义为 正则表达式 的参数。无论如何,如果发生这种情况,请使用包含在路由定义中按定义顺序的所有参数的 Request::getMatches

视图

视图是通过路由器返回和渲染包含 HTML 内容的 Response 对象到浏览器的方式。所需配置只有一个,即定义模板所在目录。

use rguezque\View;

$view = new View(
    __DIR__.'/mis_plantillas', // Directorio donde se alojan los templates
);

可以使用 View::setViewsPath 方法覆盖 View 的初始配置。

$view->setPath(__DIR__.'/templates');

注意

如果在路由器构造函数的初始配置中已定义了模板目录,则不需要在 View 类构造函数中指定它,尽管如果在此处定义了目录,则它将优先于初始配置。

设置模板

定义主模板的方法是 View::setTemplate,它可以接受一个或两个参数;第一个参数是模板文件的名称,第二个是一个关联数组,包含传递给模板的参数。

// app/Http/FooController.php
function __construct(View $view) {
    $this->view = $view;
}

public function homeAction(Request $request, Response $response): Response {
    $result = $this->view->template('home.php', ['message' => 'Hola mundo!'])->render();
    return $response->withContent($result);
}

添加参数

将参数发送到视图的另一种方法是使用 View::addArgumentView::addArguments 方法。第一个方法接受两个参数(名称和值),第二个方法接受一个关联数组。这些参数将在调用 View::render 方法时自动包含,因此必须在渲染之前声明(参见 渲染)。

// Se agrega un solo argumento
$view->addArgument('message', 'Hello weeerld!');
// Se agregan varios argumentos a la vez
$view->addArguments([
    'id' => 1,
    'name' => 'Banana',
    'color' => 'yellow'
]);

扩展模板

要扩展模板,请使用 View::extendWith 方法,该方法接受三个参数;要扩展的主模板名称,一个唯一别名,用于在主模板中包含它,以及可选地一个将发送到当前正在扩展主模板的模板的参数数组。

$data = [
    'home': '/',
    'about': '/about-us',
    'contact': '/contact-us'
];
// Se guarda el template menu.php con el alias 'menu_lateral' y se le envian parámetros en la variable $data
$view->template('index.php', ['title' => 'Ejemplo de vistas']);
$view->extendWith('menu.php', 'menu_lateral', $data);
$view->render();

接收在 $data 中发送的参数(如上代码块示例所示)

//menu.php
<nav>
    <ul>
        <li><a href="<?= $home ?>">Home</a></li>
        <li><a href="<?= $about ?>">About</a></li>
        <li><a href="<?= $contact ?>">Contact</a></li>
    </ul>
</nav>

在屏幕上打印先前使用别名 'menu_lateral' 保存的 menu.php 的内容。

// index.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $title ?></title>
</head>
<body>
    <?php
        echo $menu_lateral
    ?>
</body>
</html>

渲染视图

View::render 方法始终在最后调用,并返回当前 buffer 中的内容,以便在一个变量中检索并发送到 Response

注意

简单渲染模板的快捷方式是通过 Response::render 方法(参见 Response)。

请求

Request 类的方法。

  • fromGlobals():创建一个包含 PHP 全局变量的 Request 对象。
  • getQuery():返回 $_GET 参数数组。
  • getBody():返回 $_POST 参数数组。
  • getPhpInputStream(int $option = 0):返回未处理的 php://input 流。如果以 JSON 格式接收请求,则发送一个整型参数(JSON_DECODE = 2)并调用 getPhpInputStream(Request::JSON_DECODE);如果是字符串(PARSED_STR = 1),则调用 getPhpInputStream(Request::PARSED_STR)
  • getServer():返回 $_SERVER 参数数组。
  • getCookies():返回 $_COOKIE 参数数组。
  • getFiles():返回 $_FILES 参数数组。
  • getParams():返回请求路由的命名参数数组。
  • getParam(string $name, $default = null):返回请求路由的命名参数。
  • getMatches():返回定义在路由中的正则表达式匹配的数组。
  • setQuery(array $query):将值分配给 $_GET
  • setBody(array $body):将值分配给 $_POST
  • setServer(array $server):将值分配给 $_SERVER
  • setCookies(array $cookies):将值分配给 $_COOKIE
  • setFiles(array $files): 为$_FILES分配值。
  • setParams(array $params): 为命名参数数组分配值。
  • setParam(string $name, $value): 向命名参数数组添加一个值。
  • unsetParam(string $name): 通过名称删除一个参数。
  • setMatches(array $matches): 向正则表达式匹配数组添加值。
  • buildQuery(string $uri, array $params): 在URI中生成并返回一个GET请求字符串。

客户端请求

ClientRequest类表示客户端的HTTP请求。

use Forge\Route\ClientRequest;

// Si se omite el segundo parámetro se asume que será una petición GET
$client_request = new ClientRequest('https://jsonplaceholder.typicode.com/posts');
// Se envía la petición y se almacena la respuesta
$client_request->send();
// Se recupera el valor almacenado
$result = $client_request->toArray();

可用方法

  • withRequestMethod(string $method): 指定要进行的请求类型(GETPOSTPUTDELETE)。
  • withHeader(string $key, string $value): 向请求添加一个标题。
  • withHeaders(array $headers): 向请求添加多个标题,接收一个关联数组作为参数,其中每个键是一个标题,后跟其内容。
  • withPostFields($data, bool $encode = false): 通过一个关联数组的数据添加参数到请求中。第二个参数定义是否编码为JSON格式。
  • withBasicAuth(string $username, string $password): 添加一个基于用户名和密码的简单Authorization标题。
  • withTokenAuth(string $token): 添加一个基于JWT的Authorization标题。
  • send(): 发送请求并将响应存储。如果出错,将抛出CurlException
  • getContent(): 获取响应的值。
  • toArray(): 以JSON格式获取响应的值。
  • getInfo(): 返回一个关联数组,包含有关已发送请求的信息。如果在与ClientRequest::send()之前调用,将返回null

响应

Response类的成员方法。

  • clear(): 清除Response的值。
  • status(int $code): 分配HTTP状态码。
  • header(string $name, string $content): 向Response添加一个标题。
  • headers(array $headers): 向Response添加多个标题。
  • write($content): 向Response的正文添加内容。
  • send($data): 发送Response
  • json($data, bool $encode = true): 返回包含JSON格式内容的Response
  • render(string $template, array $arguments = []): 以模板渲染的形式返回Response。它将在路由器构造函数中定义的配置目录中查找模板。如果没有定义默认目录,则必须指定模板的完整路径。
  • redirect(string $uri): 返回一个作为重定向的Response

会话

Session类用于创建会话并管理存储在$_SESSION中的变量。通过使用new Session('sesion_name')或直接使用别名Session::select('sesion_name')来分配一个名称来初始化或选择一个变量集合。可用方法有:

  • start(): 启动会话。
  • started(): 如果会话是活动的,则返回true
  • set(string $key, $value): 创建或覆盖一个会话变量。
  • get(string $key, $default = null): 返回一个会话变量,如果不存在则返回第二个参数中指定的默认值。
  • getNamespace(): 返回当前会话命名空间的名字。
  • all(): 返回一个包含当前namespace中所有会话变量的数组。
  • has(string $key): 如果存在一个会话变量,则返回true
  • valid(string $key): 如果一个会话变量不是null且不为空,则返回true
  • remove(string $key): 删除一个会话变量。
  • clear():删除所有会话变量。
  • destroy();销毁当前会话以及相关cookie和会话变量。

服务

Services类用于注册在整个项目中使用的服务。通过Services::register方法添加服务,该方法接收两个参数,一个名称和一个匿名函数。要删除服务,使用Services::unregister,它接收要删除的服务名称(或服务,用逗号分隔)。

要将其分配给路由器,请通过Katya::setServices方法发送Services对象,从此时起,每个控制器将接收一个Services实例作为第三个参数。调用服务就像调用类的一个方法一样,或者在对象上下文中作为一个属性。

可选地,可以使用Route::useServices选择在特定路由或路由组中使用哪些服务,该方法接收已注册服务的名称,名称之间用逗号分隔。

要检查服务是否存在,使用Services::has方法(发送服务名称作为参数)和Services::names返回一个包含所有可用服务名称的数组。

require __DIR__.'/vendor/autoload.php';

use rguezque\{Group, Katya, Request, Response, Services};

$router = new Katya;
$services = new Services;

$services->register('pi', function() {
    return 3.141592654;
});
$services->register('is_pair', function(int $number) {
    return $number % 2 == 0;
});

$router->setServices($services);

$router->get('/', function(Request $request, Response $response, Services $service) {
    $pi = $service->pi(); // o bien en contexto de objeto: $service->pi
    $response->clear()->send($pi);
})->useServices('pi'); // Solamente recibirá el servicio 'pi'

变量

使用Katya::setVariables在应用程序中分配全局变量,它接收一个Variables对象作为参数。

require __DIR__.'/vendor/autoload.php';

use rguezque\{Katya, Request, Response, Variables};

$router = new Katya;
$vars = new Variables;

$vars->setVar('pi', 3.141592654);
$router->setVariables($vars);

$router->get('/', function(Request $request, Response $response, Variables $vars) {
    $response->send($vars->getVar('pi'));
});

使用Variables::setVar创建一个变量,接收变量名称和其值作为参数。

$vars->setVar('pi', 3.141592654);

使用Variables::getVar方法获取变量,接收变量名称和一个默认值,如果所调用的变量不存在;最后一个参数是可选的,如果没有声明,将返回默认值null

$vars->getVar('pi'); // Devuelve la variable pi (si no existe devuelve null)
$vars->getVar('pi', 3.14) // Devuelve la variable pi (si no existe devuelve por default el valor 3.14)

要检查变量是否存在,使用Variables::hasVar方法,如果变量存在则返回true,否则返回false

$vars->hasVar('pi') // Para este ejemplo devolvería TRUE

注意

所有变量名称都规范化为小写,并且总是作为每个控制器的最后一个参数发送,只有当它们通过Katya::setVariables定义和分配时。

数据库连接

DbConnection类通过PDO驱动程序或mysqli类提供创建与MySQL的singleton连接的方法。静态方法DbConnection::getConnection接收连接参数并返回一个包含已创建连接的对象,取决于driver参数,其中定义是否默认使用PDOmysqli

use rguezque\DbConnection;

$db = DbConnection::getConnection([
    // 'driver' => 'mysqli',
    'driver' => 'pdomysql',
    'host' => 'localhost',
    'port' => 3306,
    'user' => 'root',
    'pass' => 'mypassword',
    'dbname' => 'mydatabase'
    'charset' => 'utf8'
]);

使用URL连接

另一种选择是使用数据库URL作为连接参数,通过静态方法DbConnection::dsnParser;它接收一个URL并将其处理为以这种方式发送到DbConnection::getConnection

use rguezque\DbConnection;

// Con mysqli
// 'mysqli://root:mypassword@127.0.0.1/mydatabase?charset=utf8'
// Con PDO
$connection_params = DbConnection::dsnParser('pdomysql://root:mypassword@127.0.0.1/mydatabase?charset=utf8');
$db = DbConnection::getConnection($connection_params);

自动连接

静态方法DbConnection::autoConnect自动连接到MySQL,并自动使用在.env文件中定义的参数。

use rguezque\DbConnection;

$db = DbConnection::autoConnect();

.env文件可能看起来像这样

DB_DRIVER="mysqli"
DB_NAME="mydatabase"
DB_HOST="127.0.0.1"
DB_PORT=3306
DB_USER="root"
DB_PASS="mypassword"
DB_CHARSET="utf8"

注意

应使用某些库来处理在.env中存储的变量,并将它们加载到$_ENV变量中。最常用的是vlucas/phpdotenv

中间件

Route::before中间件在执行路由控制器之前执行一个动作。

Route::before接收一个callable对象(函数、对象方法或静态方法),在其中定义要执行的动作,该对象本身接收与控制器相同的参数:RequestResponse实例以及定义了服务时的Services实例。如果返回一个值,则通过Request对象传递给控制器,并通过Request::getParam或返回的数组从Request::getParams中检索键@middleware_data

路由和路由组都可以有中间件。如果在一个组中定义,则所有路由都将继承相同的预处理动作,但如果在一个单独的路由中定义,则该路由将优先于组的中继件。

require __DIR__.'/vendor/autoload.php';

use rguezque\{Group, Katya, Request, Response, Session};

$router = new Katya;

$router->get('/', function(Request $request, Response $response) {
    $username = $request->getParam('@data');
    $response->clear()->send(sprintf('The actual user is: %s'), $username);
})->before(function(Request $request, Response $response) {
    $session = Session::select('mi_sesion');
    if(!$session->has('logged')) {
        $response->redirect('/login');
    }

    return $session->get('username');
});

$router->group('/admin', function(Group $group) {
    $group->get('/clients', function(Request $request, Response $response) {
        // Do something
    });
    $group->get('/customers', function(Request $request, Response $response) {
        // Do something
    });
})->before(function(Request $request, Response $response) {
	$session = Session::select('mi_sesion');
    if(!$session->has('logged') || !$session->has('logged_as_admin')) {
        $response->redirect('/login');
    }
});

CORS

方法 Katya::cors 允许定义一个允许进行资源限制请求的外部域名数组,这被广泛称为 CORS (跨源资源共享)。同样,还可以指定允许的请求方法,通过将它们作为一个数组作为第二个参数传递。

require __DIR__.'/vendor/autoload.php';

use rguezque\Katya;

$router = new Katya;
// Ejemplo
$router->cors(
    [
	'(http(s)://)?(www\.)?localhost:3000'
	],
    ['GET', 'POST'] // En este ejemplo solo se permiten estos métodos
);

还可以从路由器构造函数中指定完整的CORS配置。

require __DIR__.'/vendor/autoload.php';

use rguezque\Katya;

$router = new Katya([
    'cors' => [
        'origins' => [
            '(http(s)://)?(www\.)?fakesite.com'
        ],
        'methods' => ['GET', 'POST']
    ]
]);

注意

如果在构造函数中定义了CORS配置,则无需再次使用 Katya::cors 进行配置,因为后者将不会产生效果。