edgaralexanderfr / php-espresso
PHP 运行时 Web 服务器。
This package is auto-updated.
Last update: 2024-09-24 04:01:46 UTC
README
PHP Espresso 是我创建的一个小型 PHP 框架,用于开发运行 CLI 程序和脚本的运行时 Web 服务器。与 Express(用于 NodeJS)、Gorilla Mux(用于 Golang)等框架非常相似。
重要提示:这只是一个 概念验证,用于测试 PHP 运行时服务器的可靠性,不建议在生产级项目中使用,因为它是一个用于学习目的的实验性框架。
PHP 被设计成一种 单线程 非异步 的编程语言,因此实现这种类型的 Web 服务器非常困难,因为每个请求都会存在阻塞过程,因此,这个服务器/框架是非可扩展的。
目录 📖
要求
- PHP 8.0.0 或更高版本
- 已安装并启用了 PHP 套接字模块
- Composer
- 拥有一个初始化的 Composer 项目
安装
通过 Composer 安装 PHP Espresso
composer require edgaralexanderfr/php-espresso
用法
创建基本的 Web 服务器
在您的项目中创建一个 server.php 文件,包含以下程序
<?php require_once 'vendor/autoload.php'; use Espresso\Http\Request; use Espresso\Http\Response; use Espresso\Http\Router; use Espresso\Http\Server; const PORT = 80; $server = new Server(); $router = new Router(); $router->get('/', function (Request $request, Response $response) { return $response->send([ 'message' => 'Hello world!', 'code' => 200, ]); }); $server->use($router); $server->listen(PORT, function () use ($server) { $server->log('Listening at port ' . PORT . '...'); });
运行服务器
php server.php # Use sudo if necessary for port 80
访问 https:// 或执行
curl https://
太棒了!🎉
服务器静态 HTML 页面
<?php require_once 'vendor/autoload.php'; use Espresso\Http\Request; use Espresso\Http\Response; use Espresso\Http\Router; use Espresso\Http\Server; $server = new Server(); $router = new Router(); $router->get('/php-espresso-page', function (Request $request, Response $response) { return $response->setPayload( <<<HTML <!DOCTYPE html> <html lang="en"> <head> <title>My Web Page with PHP Espresso!</title> </head> <body> <h1>My Web Page with PHP Espresso!</h1> <p>This page was served using PHP Espresso.</p> </body> </html> HTML ); }); $server->use($router); $server->listen(80, function () use ($server) { $server->log('Listening at port 80...'); });
在浏览器中访问 https:///php-espresso-page。
创建 POST 请求
<?php require_once 'vendor/autoload.php'; use Espresso\Http\Request; use Espresso\Http\Response; use Espresso\Http\Router; use Espresso\Http\Server; $server = new Server(); $router = new Router(); $router->post('/users', function (Request $request, Response $response) { $body_json = $request->getJSON(); return $response->send([ 'message' => 'User created successfully', 'code' => 201, 'user' => $body_json, ], 201); }); $server->use($router); $server->listen(80, function () use ($server) { $server->log('Listening at port 80...'); });
执行 POST 请求
curl -X POST https:///users -d '{"name":"Alexander The Great"}'
完整的 Rest API CRUD 示例
<?php require_once 'vendor/autoload.php'; use Espresso\Http\Request; use Espresso\Http\Response; use Espresso\Http\Router; use Espresso\Http\Server; /** @var stdClass[] */ $users = []; /** @var int */ $users_id = 1; $server = new Server(); $router = new Router(); $router->get('/users', function (Request $request, Response $response) use (&$users) { return $response->send($users); }); $router->get('/users/:id', function (Request $request, Response $response) use (&$users) { $id = $request->getId(); foreach ($users as $user) { if (isset($user->{'id'}) && $user->id == $id) { return $response->send($user); } } return $response->send([ 'message' => 'User not found', 'code' => 404, ], 404); }); $router->post('/users', function (Request $request, Response $response) use (&$users, &$users_id) { $body = $request->getJSON(); $email = $body->email ?? null; $name = $body->name ?? null; if (!$email || !$name) { return $response->send([ 'message' => 'Email and Name are required', 'code' => 400, ], 400); } $user = (object) [ 'id' => $users_id++, 'email' => $email, 'name' => $name, ]; $users[] = $user; return $response->send([ 'message' => 'User created successfully', 'code' => 201, 'user' => $user, ], 201); }); $router->patch('/users/:id', function (Request $request, Response $response) use (&$users) { $id = $request->getId(); $body = $request->getJSON(); foreach ($users as &$user) { if (isset($user->{'id'}) && $user->id == $id) { $user->email = $body->email ?? $user->email; $user->name = $body->name ?? $user->name; return $response->send([ 'message' => 'User updated successfully', 'code' => 200, 'user' => $user, ]); } } return $response->send([ 'message' => 'User not found', 'code' => 404, ], 404); }); $router->delete('/users/:id', function (Request $request, Response $response) use (&$users) { $id = $request->getId(); foreach ($users as $i => &$user) { if (isset($user->{'id'}) && $user->id == $id) { array_splice($users, $i, 1); return $response->send([ 'message' => 'User deleted successfully', 'code' => 200, ]); } } return $response->send([ 'message' => 'User not found', 'code' => 404, ], 404); }); $server->use($router); $server->listen(80, function () use ($server) { $server->log('Listening at port 80...'); });
创建一些用户
curl -X POST https:///users -d '{"email":"john.doe@example.com","name":"John Doe"}' curl -X POST https:///users -d '{"email":"jane.doe@example.com","name":"Jane Doe"}'
检索所有创建的用户
curl https:///users
检索 id 为 2 的用户
curl https:///users/2
更新 id 为 1 的用户
curl -X PATCH https:///users/1 -d '{"name":"John James Doe"}'
删除 id 为 2 的用户
curl -X DELETE https:///users/2
定义中间件
PHP Espresso 支持全局和路由中间件。您可以为单个路由分配任意数量的中间件。
为此,您可以创建一个新的 middlewares.php 文件并添加以下代码
<?php require_once 'vendor/autoload.php'; use Espresso\Http\Request; use Espresso\Http\Response; use Espresso\Http\Router; use Espresso\Http\Server; define('AUTH_CREDENTIALS', (object) [ 'user' => 'john.doe@example.com', 'pass' => '1234567890', // Please... don't... ]); /** * Middleware for admin authentication. */ function auth(Request $request, Response $response, callable $next) { $authorization = $request->getHeader('Authorization') ?? ''; $auth = explode(' ', $authorization); $type = $auth[0] ?? ''; $token = $auth[1] ?? ''; $credentials = explode(':', base64_decode($token)); $user = $credentials[0] ?? null; $pass = $credentials[1] ?? null; if ($type != 'Bearer' || $user != AUTH_CREDENTIALS->user || $pass != AUTH_CREDENTIALS->pass) { return $response->send([ 'message' => Espresso\Http\CODES[401], 'code' => 401, ], 401); } $next(); } /** @var stdClass[] */ $users = []; /** @var int */ $users_id = 1; $server = new Server(); $router = new Router(); // Global middleware to check service status: $server->use(function (Request $request, Response $response, callable $next) use ($argv) { $status = $argv[1] ?? ''; if ($status == 'service-closed') { return $response->send([ 'message' => 'Service unavailable temporary due to maintenance', 'code' => 503, ], 503); } $next(); }); $router->get('/users', function (Request $request, Response $response) use (&$users) { return $response->send($users); }); $router->post('/users', 'auth', function (Request $request, Response $response) use (&$users, &$users_id) { $body = $request->getJSON(); $email = $body->email ?? null; $name = $body->name ?? null; if (!$email || !$name) { return $response->send([ 'message' => 'Email and Name are required', 'code' => 400, ], 400); } $user = (object) [ 'id' => $users_id++, 'email' => $email, 'name' => $name, ]; $users[] = $user; return $response->send([ 'message' => 'User created successfully', 'code' => 201, 'user' => $user, ], 201); }); $server->use($router); $server->listen(80, function () use ($server) { $server->log('Listening at port 80...'); });
如果您运行
php middlewares.php service-closed
然后执行
curl https:///users
或者
curl -X POST https:///users -d '{"email":"john.doe@example.com","name":"John Doe"}'
您将收到以下消息
{"message":"Service unavailable temporary due to maintenance","code":503}
如果您使用 CTRL+C 杀死先前的服务器,然后运行
php middlewares.php
现在您可以检索用户列表了,例如
curl https:///users
要创建新用户,您需要经过身份验证,为此,使用 base64
将编码的 Bearer Token 分配给变量,然后将 Authorization Header
传递给 curl
命令
AUTH_TOKEN=$(echo 'john.doe@example.com:1234567890' | base64) curl -X POST https:///users -d '{"email":"john.doe@example.com","name":"John Doe"}' -H "Authorization: Bearer ${AUTH_TOKEN}"
异步编程
通过创建异步服务器并使用 async
和 $next
函数以及可调用对象,您仍然可以使用 PHP Espresso 进行异步编程
<?php require_once 'vendor/autoload.php'; use function Espresso\Event\async; use Espresso\Http\Request; use Espresso\Http\Response; use Espresso\Http\Router; use Espresso\Http\Server; const SMALLER_FILE_PATH = __DIR__ . '/files/smaller-file.txt'; const BIGGER_FILE_PATH = __DIR__ . '/files/bigger-file.txt'; function read_file(string $path, int $bytes, callable $callable = null): void { $file = fopen($path, 'r'); $file_size = filesize($path); $content = ''; $read_bytes = 0; async(function () use ($bytes, $callable, &$file, $file_size, &$content, &$read_bytes) { if ($read_bytes < $file_size) { $chunk_size = min($file_size - $read_bytes, $bytes); $chunk = fread($file, $chunk_size); $content .= $chunk; $read_bytes += $chunk_size; return false; } if ($callable) { $callable($content); } }); } $server = new Server(); $router = new Router(); $router->get('/read-file', function (Request $request, Response $response, callable $next) { $size = $request->getParam('size'); $file_path = $size == 'big' ? BIGGER_FILE_PATH : SMALLER_FILE_PATH; read_file($file_path, 8, function (string $content) use ($request, $response, $next, $size) { $response->send([ 'file_content' => $content, 'size' => $size, ]); $next(); }); }); $server->use($router); $server->async(true); $server->listen(80, function () use ($server) { $server->log('Listening at port 80...'); });
当在异步模式下运行时,async
函数会在 listen
方法中启动一个 事件循环,通过设置 $server->async(true);
。
async
可能返回一个布尔值(false),表示异步调用尚未完成,当它完成时返回 true 或 nothing。
在这个例子中,read_file
函数中的异步调用将返回 false,直到请求的文件完成,通过读取 $bytes
作为每个通过事件循环中每个调用的步骤来读取每个块,作为异步过程。
一旦整个文件被读取,$callable
回调将被调用,通过在函数的最后一个地方返回空值,将文件内容传递到 异步调用。
如果你执行
curl 'https:///read-file?size=big'\ & curl 'https:///read-file?size=small'\ & wait
尽管两个文件是同时执行的,但较小的文件请求将比较大的文件请求先响应。
这可能是实现流式、网络、数据库、文件、I/O操作等异步程序和库的一种方式,尽管它并不完美,要实现许多最初设计为 单线程 和 同步 的 PHP 库,还需要大量的工作。
也许随着 Fibers
等工具的引入,PHP 在这个目的上有着光明的未来,但我们仍将拭目以待。😄🐘