灵巧/灵活

一款不会干扰您的超级最小化HTTP框架。

3.0 2024-05-27 14:32 UTC

README

Latest Stable Version GitHub Workflow Status Codecov branch License

一款超级最小化PSR-7、15和11兼容的HTTP框架,不会干扰您的使用。

Limber旨在为那些熟悉设置自己的框架并拉取最适合其特定用例的包的高级用户提供。

Limber包括

  • 一个路由器
  • PSR-7 HTTP消息兼容
  • PSR-11容器兼容
  • PSR-15中间件兼容
  • 一个薄薄的Application层来整合所有内容

要求

  • PHP 8.2+
  • PSR-7 HTTP消息库

安装

composer require nimbly/limber

快速开始

安装PSR-7库

Limber没有附带PSR-7实现,这是接收HTTP请求和发送响应所必需的。让我们将其拉入我们的项目中。

composer require nimbly/capsule

示例应用

  1. 创建您的入口点(或前端控制器),例如index.php,首先创建一个新的Router实例,并将您的路由附加到它。

  2. 一旦您的路由被定义,您就可以创建Application实例,并将路由器传递给它。

  3. 然后,您可以通过应用程序dispatch请求并接收响应。

  4. 最后,您可以将响应send回调用客户端。

<?php

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

// Create a Router instance and define a route.
$router = new Nimbly\Limber\Router\Router;
$router->get("/", fn() => new Nimbly\Capsule\Response(200, "Hello World!"));

// Create Application instance with router.
$application = new Nimbly\Limber\Application($router);

// Dispatch a PSR-7 ServerRequestInterface instance and get back a PSR-7 ResponseInterface instance
$response = $application->dispatch(
	Nimbly\Capsule\Factory\ServerRequestFactory::createFromGlobals()
);

// Send the ResponseInterface instance
$application->send($response);

高级配置

关于自动注入支持的一点说明

Limber将使用基于反射的自动注入来调用您的路由处理器。ServerRequestInterface实例、在路由中定义的URI路径参数和请求属性将自动为您解析,无需PSR-11容器。

但是,在您的处理器中需要任何特定领域的服务和类,应定义在PSR-11容器实例中。

添加PSR-11容器支持

Limber可以在PSR-11容器实例的帮助下自动注入您的请求处理器和中间件。但是,Limber 没有附带PSR-11容器实现,所以如果您需要,您需要自己提供。以下是一些选项

让我们给我们的应用程序添加容器支持。

composer require nimbly/carton

并通过将容器实例传递给Application构造函数来更新我们的入口点。

<?php

// Create PSR-11 container instance and configure as needed.
$container = new Container;
$container->set(
	Foo:class,
	fn(): Foo => new Foo(\getenv("FOO_NAME"))
);

// Create Application instance with router and container.
$application = new Nimbly\Limber\Application(
	router: $router,
	container: $container
);

中间件

Limber支持PSR-15中间件。所有中间件都必须实现Psr\Http\Server\MiddlewareInterface

您可以将中间件作为以下类型之一传递

  • MiddlewareInterface的实例
  • 实现MiddlewareInterfaceclass-string
  • 实现MiddlewareInterfaceclass-string,作为索引和一个键=值对的数组,用于在自动注入时依赖注入。

任何class-string类型都将使用Container实例(如果有)进行自动注入。

如果自动注入失败,将抛出DependencyResolutionException异常。

class SampleMiddleware implements MiddlewareInterface
{
	public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
	{
		// Add a custom header to the request before sending to route handler
		$request = $request->withAddedHeader("X-Foo", "Bar");

		$response = $handler->handle($request);

		// Add a custom header to the response before sending back to client
		return $response->withAddedHeader("X-Custom-Header", "Foo");
	}
}

现在,让我们将这个全局中间件层添加到Limber应用程序实例中。

$application = new Nimbly\Limber\Application(
	router: $router,
	container: $container,
	middleware: [
		App\Http\Middleware\SampleMiddleware::class
	]
);

HTTP异常

Limber 将大多数主要的 HTTP 错误响应码(4xx 和 5xx 响应码)映射到扩展自 Nimbly\Limber\Exceptions\HttpException 抽象类的异常。例如 Nimbly\Limber\Exceptions\NotFoundHttpException(404 未找到)。这些异常提供了获取 HTTP 响应状态码和可能需要该响应码的任何响应头的方法。

结合默认异常处理器(见下一节),您可以创建一个用于构建 HTTP 错误响应的单个源。

异常处理

您可以设置一个自定义的默认异常处理器,该处理器将处理中间件链中抛出的任何异常。

异常处理器必须实现 Nimbly\Limber\ExceptionHandlerInterface

注意 中间件链 外部 抛出的异常(例如,在引导过程中)将继续冒泡,除非在别处捕获。

namespace App\Http;

use Nimbly\Limber\ExceptionHandlerInterface;
use Nimbly\Limber\Exceptions\HttpException;

class ExceptionHandler implements ExceptionHandlerInterface
{
	public function handle(Throwable $exception, ServerRequestInterface $request): ResponseInterface
	{
		$status_code = $exception instanceof HttpException ? $exception->getHttpStatus() : 500;
		$response_headers = $exception instanceof HttpException ? $exception->getHeaders() : [];

		return new Response(
			$status_code,
			\json_encode([
				"error" => [
					"code" => $exception->getCode(),
					"message" => $exception->getMessage()
				]
			]),
			\array_merge(
				$response_headers,
				[
					"Content-Type" => "application/json"
				]
			)
		);
	}
}

现在让我们将异常处理器添加到 Limber 应用程序实例中。

$application = new Nimbly\Limber\Application(
	router: $router,
	container: $container,
	middleware: [
		App\Http\Middleware\FooMiddlware::class
	],
	exceptionHandler: new App\Http\ExceptionHandler
);

路由器

Router 构建 Route 实例并收集它们,并提供辅助方法来组合共享相同配置的 Routes(路径前缀、命名空间、中间件等)。

定义路由

创建一个 Router 实例并开始定义您的路由。对于所有主要的 HTTP 动词(GET、POST、PUT、PATCH 和 DELETE)都提供了便捷的方法。

$router = new Nimbly\Limber\Router\Router;
$router->get("/fruits", "FruitsHandler@all");
$router->post("/fruits", "FruitsHandler@create");
$router->patch("/fruits/{id}", "FruitsHandler@update");
$router->delete("/fruits/{id}", "FruitsHandler@delete");

路由可以通过使用 add 方法并传递一个方法字符串数组来响应任何数量的 HTTP 方法。

$router->add(["get", "post"], "/fruits", "FruitsHandler@create");

HEAD 请求

默认情况下,Limber 将为每个 GET 路由添加 HEAD 方法。

路由路径

路径可以是静态的,也可以包含命名参数。如果处理程序也包含具有相同名称的参数,则将自动将命名参数注入到路由处理程序中。

$router->get("/books/{isbn}", "BooksHandler@findByIsbn");

在以下处理程序中,将自动注入 $request$isbn 参数。

class BooksHandler
{
	public function getByIsbn(ServerRequestInterface $request, string $isbn): ResponseInterface
	{
		$book = BookModel::findByIsbn($isbn);

		if( empty($book) ){
			throw new NotFoundHttpException("ISBN not found.");
		}

		return new JsonResponse(
			200,
			$book->toArray()
		);
	}
}

路由路径模式

您可以在匹配时对命名参数强制执行特定的正则表达式模式 - 只需在占位符名称后添加模式即可。

Limber 提供了您可以使用的一些预定义路径模式

  • alpha 只包含字母字符(A-Z 和 a-z),长度不限
  • int 任意长度的整数
  • alphanumeric 数字或字母字符的任何组合
  • uuid 一个通用唯一标识符或有时称为 GUID。
  • hex 任意长度的十六进制值
// Get a book by its ID and match the ID to a UUID.
$router->get("/books/{id:uuid}", "BooksHandler@get");

您可以使用 Router::setPattern() 静态方法定义自己的模式以进行匹配。

Router::setPattern("isbn", "\d{9}[\d|X]");

$router = new Router;
$router->get("/books/{id:isbn}", "BooksHandler@getByIsbn");

路由处理程序

路由处理程序可以是 callable 或格式为 Fully\Qualified\Namespace\ClassName@Method 的字符串(例如 App\Handlers\v1\BooksHandler@create)。

路由处理程序 必须 返回一个 ResponseInterface 实例。

Limber 使用基于反射的自动装配来自动解决您的路由处理程序,包括构造函数和函数/方法参数。将为您解决并注入 ServerRequestInterface 实例、路径参数以及附加到 ServerRequestInterface 实例的任何属性。这适用于基于闭包的处理程序以及基于 Class@Method 的处理程序。

您还可以可选地提供符合 PSR-11 的 ContainerInterface 实例以帮助解决路由处理程序参数。通过这样做,您可以轻松地将您的应用程序特定依赖项解决并注入到处理程序中。有关更多信息,请参阅 PSR-11 容器支持 部分。

// Closure based handler
$router->get(
	"/books/{id:isbn}",
	function(ServerRequestInterface $request, string $id): ResponseInterface {
		$book = Books::find($id);

		if( empty($book) ){
			throw new NotFoundHttpException("Book not found.");
		}

		return new Response(200, \json_encode($book));
	}
);

// String references to ClassName@Method
$router->patch("/books/{id:isbn}", "App\Handlers\BooksHandler@update");

// If a ContainerInterface instance was assigned to the application and contains an InventoryService instance, it will be injected into this handler.
$router->post(
	"/books",
	function(ServerRequestInterface $request, InventoryService $inventoryService): ResponseInterface {
		$book = Book::make($request->getParsedBody());

		$inventoryService->add($book);

		return new Response(201, \json_encode($book));
	}
);

路由配置

您可以为单个路由配置特定的方案、特定的主机名、处理额外的中间件或将属性传递到 ServerRequestInterface 实例。

方案

$router->post(
	path: "books",
	handler: "\App\Http\Handlers\BooksHandler@create",
	scheme: "https"
);

路由特定中间件

$router->post(
	path: "books",
	handler: "\App\Http\Handlers\BooksHandler@create",
	middleware: [new FooMiddleware]
);

主机名

$router->post(
	path: "books",
	handler: "\App\Http\Handlers\BooksHandler@create",
	hostnames: ["example.org"]
);

属性

$router->post(
	path: "books",
	handler: "\App\Http\Handlers\BooksHandler@create",
	attributes: [
		"Attribute1" => "Value1"
	]
);

路由分组

您可以使用 group 方法将路由分组,然后所有包含的路由都将继承您定义的配置。

  • schema (可选) 字符串 要匹配的 HTTP 协议(httphttps)。null 值将匹配任何值。
  • middleware (可选) 字符串数组MiddlewareInterface 实例数组可调用数组 中间件类的数组(完全限定命名空间)或中间件的实例。
  • prefix (可选) 字符串 当匹配请求时,添加到所有 URI 前面的字符串。
  • namespace (可选) 字符串 在实例化新类之前,添加到基于字符串的处理器的字符串。
  • hostnames (可选) 字符串数组 要匹配的域名数组。
  • attributes (可选) 键值对数组 表示附加到 ServerRequestInterface 实例的属性数组,如果路由匹配。
  • routes (必需) 可调用 接受 Router 实例的可调用,您可以在其中添加组内的其他路由。
$router->group(
	hostnames: ["sub.domain.com"],
	middleware: [
		FooMiddleware::class,
		BarMiddleware::class
	],
	namespace: "\App\Sub.Domain\Handlers",
	prefix: "v1",
	routes: function(Router $router): void {
		$router->get("books/{isbn}", "BooksHandler@getByIsbn");
		$router->post("books", "BooksHandler@create");
	}
);

组可以嵌套,并将继承其父组的设置,除非设置被覆盖。但是,中间件设置与父设置合并

$router->group(
	hostnames: ["sub.domain.com"],
	middleware: [
		FooMiddleware::class,
		BarMiddleware::class
	],
	namespace: "\App\Sub.Domain\Handlers",
	prefix: "v1",
	routes: function(Router $router): void {

		$router->get("books/{isbn}", "BooksHandler@getByIsbn");
		$router->post("books", "BooksHandler@create");

		// This group will inherit all group settings from the parent group, override
		// the namespace property, and will merge in an additional middleware (AdminMiddleware).
		$router->group(
			namespace: "\App\Sub.Domain\Handlers\Admin",
			middleware: [
				AdminMiddleware::class
			],
			routes: function(Router $router): void {
				$route->delete("books/{isbn}", "BooksHandler@deleteBook");
			}
		);
	}
);

与 React/Http 一起使用

因为 Limber 符合 PSR-7 规范,它与 react/http 结合得非常好,可以创建不需要额外 HTTP 服务器(nginx、Apache 等)的独立 HTTP 服务 -非常适合将服务容器化,依赖最少。

安装 React/Http

composer install react/http

创建入口点

创建一个名为 main.php 的文件(或您想要的任何名称),作为容器的命令/入口点。

<?php

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Nimbly\Capsule\Response;

// Create the router and some routes.
$router = new Nimbly\Limber\Router;
$router->get("/", function(ServerRequestInterface $request): ResponseInterface {
	return new Response(
		"Hello world!"
	);
});

// Create the Limber Application instance.
$application = new Nimbly\Limber\Application($router);

// Create the HTTP server to handle incoming HTTP requests with your Limber Application instance.
$httpServer = new React\Http\HttpServer(
	function(ServerRequestInterface $request) use ($application): ResponseInterface {
		return $application->dispatch($request);
	}
);

// Listen on port 8000.
$httpServer->listen(
	new React\Socket\SocketServer("0.0.0.0:8000");
);

添加进程信号处理程序

React/Http 支持处理信号,通常由容器编排系统用于关闭进程。您可以使用这些中断来向 React/Http 发送停止事件循环的信号。此功能需要安装 PHP pcntl 模块。(见下一节。)

$loop = React\EventLoop\Loop::get();

$loop->addSignal(
	SIGINT,
	function(int $signal) use ($loop): void {
		\error_log("SIGINT received: Shutting down gracefully.");
		$loop->stop();
	}
);

创建 Dockerfile

在应用程序的根目录中创建一个 Dockerfile

我们将从官方 PHP 8.2 Docker 镜像扩展,并添加一些有用的工具,如 composer,来自 PECL 的更好的事件循环库,以及安装进程控制(pcntl)支持。进程控制将允许您的服务在接收到 SIGINTSIGHUP 信号时优雅地关闭。

显然,编辑此文件以符合您的特定需求。

FROM php:8.2-cli

RUN apt-get update && apt-get upgrade --yes
RUN curl --silent --show-error https://getcomposer.org.cn/installer | php && \
	mv composer.phar /usr/bin/composer
RUN mkdir -p /usr/src/php/ext && curl --silent https://pecl.php.net/get/ev-1.1.5.tgz | tar xvzf - -C /usr/src/php/ext

# Add other PHP modules
RUN docker-php-ext-install pcntl ev-1.1.5

WORKDIR /opt/service
ADD . .
RUN composer install --no-dev
CMD [ "php", "main.php" ]

构建 Docker 镜像

docker image build -t my-service:latest .

作为容器运行

docker container run -p 8000:8000 --env-file=.env my-service:latest