FrankenPHP-Worker模式
将文件保存在内存中。 FrankenPHP Worker 将在几毫秒内处理传入的请求。
FrankenPHP-Worker 模式依赖于 Runtime 。
FrankenPHP 的 Worker 模式由 Symfony Runtime 组件 支持
Symfony Runtime 组件
https://symfony.com/doc/current/components/runtime.html
A home for runtimes. 更多的运行时案例
https://github.com/php-runtime/runtime
不用 Symfony Runtime 组件的话,我们可以 自己编写符合 PSR-7 的库实现同样的效果。
RoadRunner 同样也是支持 PSR-7 标准的库的。
PSR-7 and PSR-15 Runtime 具体实现可以看看这个
https://github.com/php-runtime/psr-17
PSR-7 Worker 代码分析
主要的运行逻辑在这段代码
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
// 保持运行 处理请求
$keepRunning = \frankenphp_handle_request($handler);
// 垃圾收集器
gc_collect_cycles();
// 保持运行
if (!$keepRunning) break;
}
其中最关键的 $handler
它的实现在源码中的
https://github.com/dunglas/frankenphp/blob/2f3e4b650b00c716028d03b853eaeb1e96747f1b/frankenphp.stub.php#L5
全部项目分布(需要登录Github)
https://github.com/search?q=repo%3Adunglas%2Ffrankenphp+frankenphp_handle_request&type=code
具体的解释
https://frankenphp.dev/docs/worker/#superglobals-behavior
frankenphp_handle_request
在第一次调用frankenphp_handle_request()
之前,超级全局变量包含与 worker 脚本本身绑定的值。这意味着在这个阶段,超级全局变量具有特定于 worker 脚本的初始值。
- 调用函数后的变化:在调用
frankenphp_handle_request()
期间及之后,超级全局变量的值会发生变化,变为从处理的 HTTP 请求生成的值。 - 每次调用这个函数都会导致超级全局变量的值被更新。
- 为了在回调中访问 worker 脚本的超级全局变量,需要复制这些变量,并将副本引入到回调的作用域中。
- 这样可以确保在回调中能够访问到 worker 脚本的超级全局变量,而不受 HTTP 请求处理过程中超级全局变量变化的影响。
// Copy worker's $_SERVER superglobal before the first call to frankenphp_handle_request()
// 在第一次调用frankenphp_handle_request()之前,复制worker 的$_SERVER超全局文件
$workerServer = $_SERVER;
$handler = static function () use ($workerServer) {
var_dump($_SERVER); // Request-bound $_SERVER
var_dump($workerServer); // $_SERVER of the worker script
};
$handler
其实 $handler
内包含了一个完整的响应请求的处理流程。
$handler = static function () {
// 使用 GuzzleHttp\Psr7\ServerRequest 从全局变量创建 PSR-7 请求
$request = ServerRequest::fromGlobals();
// 处理请求
$response = handleRequest($request);
// 发送响应
sendResponse($response);
};
或者这么写
$httpRequest = new \think\HttpExt($appExt);
$handler = static function () use ($httpRequest) {
// 初始化并处理请求
$response = $httpRequest->run();
// 发送响应
$response->send();
// 返回响应后终止应用
$http->end($response);
};
这是两种写法,总的来说就是 $handler
负责处理响应请求。frankenphp_handle_request
保持脚本的运行
Slim
Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs.
Slim是一个PHP微框架,可帮助您快速编写简单而强大的Web应用程序和API。
https://www.slimframework.com/
Github
https://github.com/slimphp/Slim
PSR-7 implementation for use with Slim 4
https://github.com/slimphp/Slim-Psr7
由于 Slim 需要使用到 PSR-7 所以我们非常方便的就能在 FrankenPHP 中以 Worker 模式运行。
安装库
composer require slim/slim:"4.*" slim/psr7
创建 index.php
// public/index.php
<?php
ignore_user_abort(true);
require __DIR__ . '/../vendor/autoload.php';
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
$app = AppFactory::create();
$handler = static function () use ($app) {
$app->get('/', function (Request $request, Response $response, $args) {
$response->getBody()->write("Hello world!");
return $response;
});
$app->get('/think', function (Request $request, Response $response, $args) {
$response->getBody()->write("Hello world! think");
return $response;
});
$app->get('/hello/{name}', function (Request $request, Response $response, $args) {
$name = $args['name'];
$response->getBody()->write("Hello, $name");
return $response;
});
$app->run();
};
// 后事处理
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler);
// 垃圾收集器
gc_collect_cycles();
// 保持运行
if (!$keepRunning) break;
}
运行
# 常规模式
frankenphp php-server --root ./public/
# Worker 模式
frankenphp php-server --worker ./public/index.php --root ./public/
ThinkPHP
参考 https://blog.csdn.net/bennybi/article/details/142883918
<?php
ignore_user_abort(true);
require __DIR__ . '/../vendor/autoload.php';
$thinkApp = new \think\App();
$http = $thinkApp->http;
$handler = static function () use ($http) {
// 初始化并处理请求
$response = $http->run();
// 发送响应
$response->send();
// 返回响应后终止应用
$http->end($response);
};
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
// 处理请求
$keepRunning = \frankenphp_handle_request($handler);
// 执行一些在发送 HTTP 响应后的操作
gc_collect_cycles(); // 垃圾回收
if (!$keepRunning) break;
}
使用普通模式运行后会报错
Uncaught TypeError: frankenphp_handle_request(): Argument #1 ($callback) must be a valid callback, no array or string given in
frankenphp_handle_request()
只有在 Worker 模式下才会生效
Nyholm/PSR7
https://github.com/Nyholm/psr7
https://github.com/Nyholm/psr7-server
composer require nyholm/psr7 nyholm/psr7-server
index.php
<?php
ignore_user_abort(true);
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
use Psr\Http\Message\ResponseInterface;
require __DIR__ . '/../vendor/autoload.php';
// 创建 PSR-17 工厂
$psr17Factory = new Psr17Factory();
// 创建 ServerRequestCreator(将 PHP 原生请求转换为 PSR-7 请求)
$serverRequestCreator = new ServerRequestCreator(
$psr17Factory, // ServerRequestFactory
$psr17Factory, // UriFactory
$psr17Factory, // UploadedFileFactory
$psr17Factory // StreamFactory
);
// 将全局变量 ($_SERVER, $_POST, 等) 构建为 PSR-7 ServerRequest 对象
$serverRequest = $serverRequestCreator->fromGlobals();
// 处理请求
$handler = static function () use ($serverRequest) {
// 处理请求逻辑
$response = handleRequest($serverRequest);
// 输出响应
sendResponse($response);
};
/**
* 请求处理逻辑
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return ResponseInterface
*/
function handleRequest(Psr\Http\Message\ServerRequestInterface $request): ResponseInterface
{
$psr17Factory = new Psr17Factory();
// 示例:根据请求路径返回不同的内容
$path = $request->getUri()->getPath();
print_r($path);
if ($path === '/') {
$body = $psr17Factory->createStream('Hello, welcome to the homepage!');
return $psr17Factory->createResponse(200)
->withBody($body)
->withHeader('Content-Type', 'text/plain');
} elseif ($path === '/about') {
$body = $psr17Factory->createStream('This is the about page.');
return $psr17Factory->createResponse(200)
->withBody($body)
->withHeader('Content-Type', 'text/plain');
} else {
$body = $psr17Factory->createStream('Page not found.');
return $psr17Factory->createResponse(404)
->withBody($body)
->withHeader('Content-Type', 'text/plain');
}
}
/**
* 输出响应到客户端
* @param ResponseInterface $response
*/
function sendResponse(ResponseInterface $response): void
{
// 设置 HTTP 状态码
http_response_code($response->getStatusCode());
// 设置响应头
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header(sprintf('%s: %s', $name, $value), false);
}
}
// 输出响应体
echo $response->getBody();
}
// frankenphp
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
// 保持运行 处理请求
$keepRunning = \frankenphp_handle_request($handler);
// 垃圾收集器
gc_collect_cycles();
// 保持运行
if (!$keepRunning) break;
}
Slim
Slim 框架在其 AppFactory
中默认支持自定义的 PSR-7 工厂,因此我们可以在配置中传递 Nyholm\Psr7\Factory\Psr17Factory
来替代默认的实现。
composer require slim/slim nyholm/psr7 nyholm/psr7-server
index.php
<?php
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
use Slim\Factory\AppFactory;
require __DIR__ . '/vendor/autoload.php';
// 使用 Nyholm 的 PSR-17 工厂
$psr17Factory = new Psr17Factory();
// 配置 Slim 使用 Nyholm 的 PSR-7 实现
AppFactory::setResponseFactory($psr17Factory);
AppFactory::setPsr17Factory($psr17Factory);
// 创建 Slim App 实例
$app = AppFactory::create();
// 定义一个简单的路由
$app->get('/', function ($request, $response, $args) {
$response->getBody()->write("Hello, this is the homepage!");
return $response;
});
$app->get('/about', function ($request, $response, $args) {
$response->getBody()->write("This is the about page.");
return $response;
});
// 手动使用 Nyholm 的 ServerRequestCreator 来生成 PSR-7 请求对象
$serverRequestCreator = new ServerRequestCreator(
$psr17Factory, // ServerRequestFactory
$psr17Factory, // UriFactory
$psr17Factory, // UploadedFileFactory
$psr17Factory // StreamFactory
);
$request = $serverRequestCreator->fromGlobals(); // 从 PHP 超全局变量构建 PSR-7 请求
// 运行 Slim 应用
$app->handle($request)->send();
GuzzleHttp/Psr7
composer require guzzlehttp/psr7
<?php
ignore_user_abort(true);
require __DIR__ . '/../vendor/autoload.php';
use GuzzleHttp\Psr7\ServerRequest;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
/**
* Worker 模式入口文件
*/
function handleRequest(ServerRequest $request): ResponseInterface
{
// 获取请求的路径
$path = $request->getUri()->getPath();
// 处理不同的路径
switch ($path) {
case '/':
$body = "Hello, welcome to the homepage!";
return new Response(200, ['Content-Type' => 'text/plain'], $body);
case '/about':
$body = "This is the about page.";
return new Response(200, ['Content-Type' => 'text/plain'], $body);
default:
$body = "404 Not Found";
return new Response(404, ['Content-Type' => 'text/plain'], $body);
}
}
/**
* 将响应发送到客户端
*/
function sendResponse(ResponseInterface $response): void
{
// 设置 HTTP 状态码
http_response_code($response->getStatusCode());
// 设置响应头
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header(sprintf('%s: %s', $name, $value), false);
}
}
// 输出响应体
echo $response->getBody();
}
// 请求处理
$handler = static function () use ($request) {
// 使用 GuzzleHttp\Psr7\ServerRequest 从全局变量创建 PSR-7 请求
$request = ServerRequest::fromGlobals();
// 处理请求
$response = handleRequest($request);
// 发送响应
sendResponse($response);
};
// 后事处理
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
// 保持运行 处理请求
$keepRunning = \frankenphp_handle_request($handler);
// 垃圾收集器
gc_collect_cycles();
// 保持运行
if (!$keepRunning) break;
}
Slim
composer require slim/slim guzzlehttp/psr7
index.php
<?php
require __DIR__ . '/vendor/autoload.php';
use Slim\Factory\AppFactory;
use GuzzleHttp\Psr7\ServerRequest;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
// 配置 Slim 框架使用 GuzzleHttp/Psr7 的 PSR-7 实现
AppFactory::setResponseFactory(new class extends Response {
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface {
return new Response($code, reasonPhrase: $reasonPhrase);
}
});
$app = AppFactory::create();
// 定义一个简单的路由
$app->get('/', function (ServerRequest $request, Response $response, array $args) {
$response->getBody()->write('Hello, welcome to the homepage powered by GuzzleHttp/Psr7!');
return $response;
});
$app->get('/about', function (ServerRequest $request, Response $response, array $args) {
$response->getBody()->write('This is the about page.');
return $response;
});
// 定义动态路由
$app->get('/hello/{name}', function (ServerRequest $request, Response $response, array $args) {
$name = $args['name'];
$response->getBody()->write("Hello, $name!");
return $response;
});
// 运行 Slim 应用
$app->run();
扩展功能
1. 中间件支持
Slim 框架支持 PSR-15 中间件,因此可以在应用中添加中间件。例如,一个简单的日志中间件:
$app->add(function (ServerRequest $request, $handler) {
$response = $handler->handle($request);
$response->getBody()->write("\nMiddleware executed.");
return $response;
});
2. 查询参数处理
通过 ServerRequest
的 getQueryParams()
方法,可以访问 URL 中的查询参数:
$app->get('/search', function (ServerRequest $request, Response $response, array $args) {
$params = $request->getQueryParams();
$query = $params['q'] ?? 'nothing';
$response->getBody()->write("You searched for: $query");
return $response;
});
访问 http://127.0.0.1:8000/search?q=Slim
会返回:You searched for: Slim
3. JSON API
返回 JSON 数据时,可以设置响应头为 application/json
:
$app->get('/api/data',
function (ServerRequest $request, Response $response, array $args) {
$data = ['name' => 'John', 'age' => 30];
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
});
访问 http://127.0.0.1:8000/api/data
会返回:{ "name": "John", "age": 30 }
Laravel
Lavavel docs
https://docs.golaravel.com/docs/
frankenphp 参考
https://frankenphp.dev/docs/laravel/#laravel-octane
安装 lavavel
composer create-project laravel/laravel laravel-app
composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:frankenphp
/public/frankenphp-worker.php
创建
// Set a default for the application base path and public path if they are missing...
$_SERVER['APP_BASE_PATH'] = $_ENV['APP_BASE_PATH'] ?? $_SERVER['APP_BASE_PATH'] ?? __DIR__.'/..';
$_SERVER['APP_PUBLIC_PATH'] = $_ENV['APP_PUBLIC_PATH'] ?? $_SERVER['APP_BASE_PATH'] ?? __DIR__;
require __DIR__.'/../vendor/laravel/octane/bin/frankenphp-worker.php';
核心文件app/laravel-app/vendor/laravel/octane/bin/frankenphp-worker.php
判断是否在模式内
if ((! ($_SERVER['FRANKENPHP_WORKER'] ?? false)) || ! function_exists('frankenphp_handle_request')) {
echo 'FrankenPHP must be in worker mode to use this script.';
exit(1);
}
核心文件
<?php
use Laravel\Octane\ApplicationFactory;
use Laravel\Octane\FrankenPhp\FrankenPhpClient;
use Laravel\Octane\RequestContext;
use Laravel\Octane\Stream;
use Laravel\Octane\Worker;
use Symfony\Component\HttpFoundation\Response;
if ((! ($_SERVER['FRANKENPHP_WORKER'] ?? false)) || ! function_exists('frankenphp_handle_request')) {
echo 'FrankenPHP must be in worker mode to use this script.';
exit(1);
}
ignore_user_abort(true);
$basePath = require __DIR__.'/bootstrap.php';
/*
|--------------------------------------------------------------------------
| Start The Octane Worker
|--------------------------------------------------------------------------
|
| Next we will start the Octane worker, which is a long running process to
| handle incoming requests to the application. This worker will be used
| by FrankenPHP to serve an entire Laravel application at high speed.
|
*/
$frankenPhpClient = new FrankenPhpClient();
$worker = null;
$requestCount = 0;
$maxRequests = $_ENV['MAX_REQUESTS'] ?? $_SERVER['MAX_REQUESTS'] ?? 1000;
$requestMaxExecutionTime = $_ENV['REQUEST_MAX_EXECUTION_TIME'] ?? $_SERVER['REQUEST_MAX_EXECUTION_TIME'] ?? null;
if (PHP_OS_FAMILY === 'Linux' && ! is_null($requestMaxExecutionTime)) {
set_time_limit((int) $requestMaxExecutionTime);
}
try {
$handleRequest = static function () use (&$worker, $basePath, $frankenPhpClient) {
try {
$worker ??= tap(
new Worker(
new ApplicationFactory($basePath), $frankenPhpClient
)
)->boot();
[$request, $context] = $frankenPhpClient->marshalRequest(new RequestContext());
$worker->handle($request, $context);
} catch (Throwable $e) {
if ($worker) {
report($e);
}
$response = new Response(
'Internal Server Error',
500,
[
'Status' => '500 Internal Server Error',
'Content-Type' => 'text/plain',
],
);
$response->send();
Stream::shutdown($e);
}
};
while ($requestCount < $maxRequests && frankenphp_handle_request($handleRequest)) {
$requestCount++;
}
} finally {
$worker?->terminate();
gc_collect_cycles();
}
Lumen
https://lumen.golaravel.com/docs/
composer create-project laravel/lumen laravel-lumen
require __DIR__.'/../vendor/autoload.php';
$handler = static function () {
$app = require __DIR__.'/../bootstrap/app.php';
$app->run();
};
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
// 保持运行 处理请求
$keepRunning = \frankenphp_handle_request($handler);
// 垃圾收集器
gc_collect_cycles();
// 保持运行
if (!$keepRunning) break;
}