将文件保存在内存中。 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. 查询参数处理

通过 ServerRequestgetQueryParams() 方法,可以访问 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;
}

标签: PHP, FrankenPHP

添加新评论