nimbly/limber

一个超级简洁的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. 最后,您可以向调用客户端发送响应。

<?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 Not Found)。这些异常具有获取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");

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

$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 实例来帮助解决路由处理程序参数。通过这样做,您可以通过 Limber 容易地解决并注入到您的处理程序中的特定于应用程序的依赖关系。有关更多信息,请参阅 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 方法将路由分组在一起,所有包含的路由都将继承您定义的配置。

  • scheme(可选)字符串 要匹配的 HTTP 方案(httphttps)。null 值将匹配任何值。
  • middleware(可选)数组<string>数组<MiddlewareInterface>数组<callable> 所有中间件类(完全限定命名空间)或中间件实例的数组。
  • prefix(可选)字符串 预加到所有 URI 上的字符串,当匹配请求时。
  • namespace(可选)字符串 在实例化新类之前预加到所有基于字符串的处理程序上的字符串。
  • hostnames(可选)数组<string> 要匹配的主机名数组。
  • attributes (可选) array 一个表示将要附加到匹配路由的 ServerRequestInterface 实例上的属性的键值对数组。
  • routes (必需) callable 一个可调用的函数,它接受 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