edgaralexanderfr/php-espresso

PHP 运行时 Web 服务器。

v1.0.0 2023-03-12 02:18 UTC

This package is auto-updated.

Last update: 2024-09-24 04:01:46 UTC


README

PHP Espresso Example GIF

View last release PHP 8.0.0 Experimental Sockets Composer

PHP Espresso 是我创建的一个小型 PHP 框架,用于开发运行 CLI 程序和脚本的运行时 Web 服务器。与 Express(用于 NodeJS)、Gorilla Mux(用于 Golang)等框架非常相似。

重要提示:这只是一个 概念验证,用于测试 PHP 运行时服务器的可靠性,不建议在生产级项目中使用,因为它是一个用于学习目的的实验性框架。

PHP 被设计成一种 单线程 非异步 的编程语言,因此实现这种类型的 Web 服务器非常困难,因为每个请求都会存在阻塞过程,因此,这个服务器/框架是非可扩展的。

目录 📖
  1. 要求
  2. 安装
  3. 用法

要求

  1. PHP 8.0.0 或更高版本
  2. 已安装并启用了 PHP 套接字模块
  3. Composer
  4. 拥有一个初始化的 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),表示异步调用尚未完成,当它完成时返回 truenothing

在这个例子中,read_file 函数中的异步调用将返回 false,直到请求的文件完成,通过读取 $bytes 作为每个通过事件循环中每个调用的步骤来读取每个块,作为异步过程。

一旦整个文件被读取,$callable 回调将被调用,通过在函数的最后一个地方返回空值,将文件内容传递到 异步调用

如果你执行

curl 'https:///read-file?size=big'\
& curl 'https:///read-file?size=small'\
& wait

尽管两个文件是同时执行的,但较小的文件请求将比较大的文件请求先响应。

这可能是实现流式、网络、数据库、文件、I/O操作等异步程序和库的一种方式,尽管它并不完美,要实现许多最初设计为 单线程同步PHP 库,还需要大量的工作。

也许随着 Fibers 等工具的引入,PHP 在这个目的上有着光明的未来,但我们仍将拭目以待。😄🐘