hollodotme/fast-cgi-client

一个用于发送请求(同步)到PHP-FPM的PHP快速CGI客户端。

v3.1.7 2021-12-07 10:10 UTC

README

FastCGI Client CI PHP 7.1 - 8.1 Latest Stable Version Total Downloads

快速CGI客户端

使用FastCGI协议,将请求(同步)发送到PHP-FPM的PHP快速CGI客户端。

这个库基于Pierrick CharronPHP-FastCGI-Client工作,并将其移植并现代化以适应最新的PHP版本,增加了处理多个请求(循环中)的功能,以及单元和集成测试。

这是最新版本的文档。

请查看变更日志中的向后不兼容更改(BC breaks)

以下链接提供了早期版本的详细信息

这篇博客文章中了解更多关于前往 v2.6.0 的旅程和更改。

您可以在我的相关博客文章中找到一个实验性用例。

您还可以在我的speakerdeck.com上的演讲中找到关于此项目的幻灯片。

安装

composer require hollodotme/fast-cgi-client

使用 - 连接

此库支持两种连接到FastCGI服务器的方式

  1. 通过网络套接字
  2. 通过Unix域套接字

创建网络套接字连接

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\SocketConnections\NetworkSocket;

$connection = new NetworkSocket(
	'127.0.0.1',    # Hostname
	9000,           # Port
	5000,           # Connect timeout in milliseconds (default: 5000)
	5000            # Read/write timeout in milliseconds (default: 5000)
);

创建Unix域套接字连接

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\SocketConnections\UnixDomainSocket;

$connection = new UnixDomainSocket(
	'/var/run/php/php7.3-fpm.sock',     # Socket path
	5000,                               # Connect timeout in milliseconds (default: 5000)
	5000                                # Read/write timeout in milliseconds (default: 5000)
);

使用 - 单个请求

以下示例假设 /path/to/target/script.php 的内容如下所示

<?php declare(strict_types=1);

sleep((int)($_REQUEST['sleep'] ?? 0));
echo $_REQUEST['key'] ?? '';

同步发送请求

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;

$client     = new Client();
$connection = new NetworkSocket('127.0.0.1', 9000);
$content    = http_build_query(['key' => 'value']);
$request    = new PostRequest('/path/to/target/script.php', $content);

$response = $client->sendRequest($connection, $request);

echo $response->getBody();
# prints
value

异步发送请求(触发后忘记)

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;

$client     = new Client();
$connection = new NetworkSocket('127.0.0.1', 9000);
$content    = http_build_query(['key' => 'value']);
$request    = new PostRequest('/path/to/target/script.php', $content);

$socketId = $client->sendAsyncRequest($connection, $request);

echo "Request sent, got ID: {$socketId}";

发送异步请求后读取响应

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;

$client     = new Client();
$connection = new NetworkSocket('127.0.0.1', 9000);
$content    = http_build_query(['key' => 'value']);
$request    = new PostRequest('/path/to/target/script.php', $content);

$socketId = $client->sendAsyncRequest($connection, $request);

echo "Request sent, got ID: {$socketId}";

# Do something else here in the meanwhile

# Blocking call until response is received or read timed out
$response = $client->readResponse( 
	$socketId,     # The socket ID 
	3000            # Optional timeout to wait for response,
					# defaults to read/write timeout in milliseconds set in connection
);

echo $response->getBody();
# prints
value

当异步请求响应时通知回调

您可以为每个请求注册响应和失败回调。为了在收到响应时通知回调而不是返回它,您需要使用 waitForResponse(int $socketId, ?int $timeoutMs = null) 方法。

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\Interfaces\ProvidesResponseData;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;
use Throwable;

$client     = new Client();
$connection = new NetworkSocket('127.0.0.1', 9000);
$content    = http_build_query(['key' => 'value']);
$request    = new PostRequest('/path/to/target/script.php', $content);

# Register a response callback, expects a `ProvidesResponseData` instance as the only parameter
$request->addResponseCallbacks(
	static function( ProvidesResponseData $response )
	{
		echo $response->getBody();	
	}
);

# Register a failure callback, expects a `\Throwable` instance as the only parameter
$request->addFailureCallbacks(
	static function ( Throwable $throwable )
	{
		echo $throwable->getMessage();	
	}
);

$socketId = $client->sendAsyncRequest($connection, $request);

echo "Request sent, got ID: {$socketId}";

# Do something else here in the meanwhile

# Blocking call until response is received or read timed out
# If response was received all registered response callbacks will be notified
$client->waitForResponse( 
	$socketId,     # The socket ID 
	3000            # Optional timeout to wait for response,
					# defaults to read/write timeout in milliseconds set in connection
);

# ... is the same as

while(true)
{
	if ($client->hasResponse($socketId))
	{
		$client->handleResponse($socketId, 3000);
		break;
	}
}
# prints
value

使用 - 多个请求

发送多个请求并读取它们的响应(顺序保留)

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;

$client     = new Client();
$connection = new NetworkSocket('127.0.0.1', 9000);

$request1 = new PostRequest('/path/to/target/script.php', http_build_query(['key' => '1']));
$request2 = new PostRequest('/path/to/target/script.php', http_build_query(['key' => '2']));
$request3 = new PostRequest('/path/to/target/script.php', http_build_query(['key' => '3']));

$socketIds = [];

$socketIds[] = $client->sendAsyncRequest($connection, $request1);
$socketIds[] = $client->sendAsyncRequest($connection, $request2);
$socketIds[] = $client->sendAsyncRequest($connection, $request3);

echo 'Sent requests with IDs: ' . implode( ', ', $socketIds ) . "\n";

# Do something else here in the meanwhile

# Blocking call until all responses are received or read timed out
# Responses are read in same order the requests were sent
foreach ($client->readResponses(3000, ...$socketIds) as $response)
{
	echo $response->getBody() . "\n";	
}
# prints
1
2
3

发送多个请求并读取它们的响应(响应式)

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;

$client     = new Client();
$connection = new NetworkSocket('127.0.0.1', 9000);

$request1 = new PostRequest('/path/to/target/script.php', http_build_query(['key' => '1', 'sleep' => 3]));
$request2 = new PostRequest('/path/to/target/script.php', http_build_query(['key' => '2', 'sleep' => 2]));
$request3 = new PostRequest('/path/to/target/script.php', http_build_query(['key' => '3', 'sleep' => 1]));

$socketIds = [];

$socketIds[] = $client->sendAsyncRequest($connection, $request1);
$socketIds[] = $client->sendAsyncRequest($connection, $request2);
$socketIds[] = $client->sendAsyncRequest($connection, $request3);

echo 'Sent requests with IDs: ' . implode( ', ', $socketIds ) . "\n";

# Do something else here in the meanwhile

# Loop until all responses were received
while ( $client->hasUnhandledResponses() )
{
	# read all ready responses
	foreach ( $client->readReadyResponses( 3000 ) as $response )
	{
		echo $response->getBody() . "\n";
	}
	
	echo '.';
}

# ... is the same as

while ( $client->hasUnhandledResponses() )
{
	$readySocketIds = $client->getSocketIdsHavingResponse();
	
	# read all ready responses
	foreach ( $client->readResponses( 3000, ...$readySocketIds ) as $response )
	{
		echo $response->getBody() . "\n";
	}
	
	echo '.';
}

# ... is the same as

while ( $client->hasUnhandledResponses() )
{
	$readySocketIds = $client->getSocketIdsHavingResponse();
	
	# read all ready responses
	foreach ($readySocketIds as $socketId)
	{
		$response = $client->readResponse($socketId, 3000);
		echo $response->getBody() . "\n";
	}
	
	echo '.';
}
# prints
...............................................3
...............................................2
...............................................1

发送多个请求并通知回调(响应式)

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\Interfaces\ProvidesResponseData;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;
use Throwable;

$client     = new Client();
$connection = new NetworkSocket('127.0.0.1', 9000);

$responseCallback = static function( ProvidesResponseData $response )
{
	echo $response->getBody();	
};

$failureCallback = static function ( Throwable $throwable )
{
	echo $throwable->getMessage();	
};

$request1 = new PostRequest('/path/to/target/script.php', http_build_query(['key' => '1', 'sleep' => 3]));
$request2 = new PostRequest('/path/to/target/script.php', http_build_query(['key' => '2', 'sleep' => 2]));
$request3 = new PostRequest('/path/to/target/script.php', http_build_query(['key' => '3', 'sleep' => 1]));

$request1->addResponseCallbacks($responseCallback);
$request1->addFailureCallbacks($failureCallback);

$request2->addResponseCallbacks($responseCallback);
$request2->addFailureCallbacks($failureCallback);

$request3->addResponseCallbacks($responseCallback);
$request3->addFailureCallbacks($failureCallback);

$socketIds = [];

$socketIds[] = $client->sendAsyncRequest($connection, $request1);
$socketIds[] = $client->sendAsyncRequest($connection, $request2);
$socketIds[] = $client->sendAsyncRequest($connection, $request3);

echo 'Sent requests with IDs: ' . implode( ', ', $socketIds ) . "\n";

# Do something else here in the meanwhile

# Blocking call until all responses were received and all callbacks notified
$client->waitForResponses(3000);

# ... is the same as

while ( $client->hasUnhandledResponses() )
{
	$client->handleReadyResponses(3000);
}

# ... is the same as

while ( $client->hasUnhandledResponses() )
{
	$readySocketIds = $client->getSocketIdsHavingResponse();
	
	# read all ready responses
	foreach ($readySocketIds as $socketId)
	{
		$client->handleResponse($socketId, 3000);
	}
}
# prints
3
2
1

使用透传回调从工作脚本中读取输出缓冲区

查看请求脚本的执行进度,可以通过访问该脚本的输出缓冲区来很有用。php-fpm 的 php.ini 默认输出缓冲区大小为 4096 字节,并且对于 CLI 模式是(硬编码)禁用的。(查看文档) 调用 ob_implicit_flush() 会使每个对 echoprint 的调用立即刷新。

被调用脚本可能如下所示

<?php declare(strict_types=1);

ob_implicit_flush();

function show( string $string )
{
	echo $string . str_repeat( "\r", 4096 - strlen( $string ) ) . "\n";
	sleep( 1 );
}

show( 'One' );
show( 'Two' );
show( 'Three' );

error_log("Oh oh!\n");

echo 'End';

调用者可能如下所示

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Requests\GetRequest;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;

$client     = new Client();
$connection = new NetworkSocket('127.0.0.1', 9000);

$passThroughCallback = static function( string $outputBuffer, string $errorBuffer )
{
	echo 'Output: ' . $outputBuffer;
	echo 'Error: ' . $errorBuffer;
};

$request = new GetRequest('/path/to/target/script.php', '');
$request->addPassThroughCallbacks( $passThroughCallback );

$client->sendAsyncRequest($connection, $request);
$client->waitForResponses();
# prints immediately
Buffer: Content-type: text/html; charset=UTF-8

Output: One
# sleeps 1 sec
Output: Two
# sleeps 1 sec
Output: Three
# sleeps 1 sec
Error: Oh oh!
Output: End

请求

请求由以下接口定义

<?php declare(strict_types=1);

namespace hollodotme\FastCGI\Interfaces;

interface ProvidesRequestData
{
	public function getGatewayInterface() : string;

	public function getRequestMethod() : string;

	public function getScriptFilename() : string;

	public function getServerSoftware() : string;

	public function getRemoteAddress() : string;

	public function getRemotePort() : int;

	public function getServerAddress() : string;

	public function getServerPort() : int;

	public function getServerName() : string;

	public function getServerProtocol() : string;

	public function getContentType() : string;

	public function getContentLength() : int;

	public function getContent() : string;

	public function getCustomVars() : array;

	public function getParams() : array;
	
	public function getRequestUri() : string;
}

除了这个接口之外,这个包还提供了一个抽象请求类,包含默认值,使 API 对您更方便,并提供这个抽象类的 5 个请求方法实现

  • hollodotme\FastCGI\Requests\GetRequest
  • hollodotme\FastCGI\Requests\PostRequest
  • hollodotme\FastCGI\Requests\PutRequest
  • hollodotme\FastCGI\Requests\PatchRequest
  • hollodotme\FastCGI\Requests\DeleteRequest

因此,您可以实现接口,从抽象类继承,或者简单地使用这 5 个实现中的任何一个。

默认值

抽象请求类定义了几个默认值,您可以选择覆盖这些值

请求内容

为了使不同请求内容类型的组合更容易,有一些类涵盖了典型的内容类型

您可以通过实现以下接口创建自己的请求内容类型组合器

ComposesRequestContent

interface ComposesRequestContent
{
	public function getContentType() : string;

	public function getContent() : string;
}
请求内容示例:URL 编码表单数据(application/x-www-form-urlencoded)
<?php declare(strict_types=1);

use hollodotme\FastCGI\RequestContents\UrlEncodedFormData;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\Client;

$client = new Client();
$connection = new NetworkSocket( '127.0.0.1', 9000 );

$urlEncodedContent = new UrlEncodedFormData(
	[
		'nested' => [
			'one',
			'two'   => 'value2',
			'three' => [
				'value3',
			],
		],
	]
);

$postRequest = PostRequest::newWithRequestContent( '/path/to/target/script.php', $urlEncodedContent );

$response = $client->sendRequest( $connection, $postRequest );

此示例在目标脚本中生成以下 $_POST 数组

Array
(
    [nested] => Array
        (
            [0] => one
            [two] => value2
            [three] => Array
                (
                    [0] => value3
                )

        )
)
请求内容示例:多部分表单数据(multipart/form-data)

多部分表单数据可以用来将任何二进制数据作为文件传输到目标脚本,就像浏览器中的文件上传一样。

请注意:多部分表单数据内容类型仅与 POST 请求一起使用。

<?php declare(strict_types=1);

use hollodotme\FastCGI\RequestContents\MultipartFormData;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\Client;

$client = new Client();
$connection = new NetworkSocket( '127.0.0.1', 9000 );

$multipartContent = new MultipartFormData(
    # POST data
	[
        'simple'          => 'value',
		'nested[]'        => 'one',
		'nested[two]'     => 'value2',
		'nested[three][]' => 'value3',
	],
	# FILES
	[
		'file1'        => __FILE__,
		'files[1]'     => __FILE__,
		'files[three]' => __FILE__,
	]
);

$postRequest = PostRequest::newWithRequestContent( '/path/to/target/script.php', $multipartContent );

$response = $client->sendRequest( $connection, $postRequest );

此示例在目标脚本中生成以下 $_POST$_FILES 数组

# $_POST
Array
(
    [simple] => value
    [nested] => Array
        (
            [0] => one
            [two] => value2
            [three] => Array
                (
                    [0] => value3
                )

        )

)

# $_FILES
Array
(
    [file1] => Array
        (
            [name] => multipart.php
            [type] => application/octet-stream
            [tmp_name] => /tmp/phpiIdCNM
            [error] => 0
            [size] => 1086
        )

    [files] => Array
        (
            [name] => Array
                (
                    [1] => multipart.php
                    [three] => multipart.php
                )

            [type] => Array
                (
                    [1] => application/octet-stream
                    [three] => application/octet-stream
                )

            [tmp_name] => Array
                (
                    [1] => /tmp/phpAjHINL
                    [three] => /tmp/phpicAmjN
                )

            [error] => Array
                (
                    [1] => 0
                    [three] => 0
                )

            [size] => Array
                (
                    [1] => 1086
                    [three] => 1086
                )

        )

)
请求内容示例:JSON 编码数据(application/json)
<?php declare(strict_types=1);

use hollodotme\FastCGI\RequestContents\JsonData;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\Client;

$client = new Client();
$connection = new NetworkSocket( '127.0.0.1', 9000 );

$jsonContent = new JsonData(
	[
		'nested' => [
			'one',
			'two'   => 'value2',
			'three' => [
				'value3',
			],
		],
	]
);

$postRequest = PostRequest::newWithRequestContent( '/path/to/target/script.php', $jsonContent );

$response = $client->sendRequest( $connection, $postRequest );

此示例在目标脚本中生成以下 php://input 内容

{
  "nested": {
    "0": "one",
    "two": "value2",
    "three": [
      "value3"
    ]
  }
}

响应

响应由以下接口定义

<?php declare(strict_types=1);

namespace hollodotme\FastCGI\Interfaces;

interface ProvidesResponseData
{
	public function getHeaders() : array;

	public function getHeader( string $headerKey ) : array;
	
	public function getHeaderLine( string $headerKey ) : string;

	public function getBody() : string;

	public function getOutput() : string;
	
	public function getError() : string;

	public function getDuration() : float;
}

假设 /path/to/target/script.php 有以下内容

<?php declare(strict_types=1);

echo 'Hello World';
error_log('Some error');

原始响应将如下所示

Content-type: text/html; charset=UTF-8

Hello World

请注意

  • 您脚本发送的所有头都会在响应体之前
  • 不会包含任何 HTTP 特定的头,如 HTTP/1.1 200 OK,因为没有涉及 web 服务器。

自定义头也将是响应的一部分

<?php declare(strict_types=1);

header('X-Custom: Header');
header('Set-Cookie: yummy_cookie=choco');
header('Set-Cookie: tasty_cookie=strawberry');

echo 'Hello World';
error_log('Some error');

原始响应将如下所示

X-Custom: Header
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
Content-type: text/html; charset=UTF-8

Hello World

您可以从响应对象中单独检索所有响应数据

# Get all values of a single response header
$response->getHeader('Set-Cookie'); 
// ['yummy_cookie=choco', 'tasty_cookie=strawberry']

# Get all values of a single response header as comma separated string
$response->getHeaderLine('Set-Cookie');
// 'yummy_cookie=choco, tasty_cookie=strawberry'

# Get all headers as grouped array
$response->getHeaders();
// [
//   'X-Custom' => [
//      'Header',
//   ],
//   'Set-Cookie' => [
//      'yummy_cookie=choco',
//      'tasty_cookie=strawberry',
//   ],
//   'Content-type' => [
//      'text/html; charset=UTF-8',
//   ],
// ]

# Get the body
$response->getBody(); 
// 'Hello World'

# Get the raw response output from STDOUT stream
$response->getOutput();
// 'X-Custom: Header
// Set-Cookie: yummy_cookie=choco
// Set-Cookie: tasty_cookie=strawberry
// Content-type: text/html; charset=UTF-8
// 
// Hello World'

# Get the raw response from SFTERR stream
$response->getError();
// Some error

# Get the duration
$response->getDuration(); 
// e.g. 0.0016319751739502

故障排除

"文件未找到。" 响应(php-fpm)

此响应由 php-fpm 在以下错误 Primary script unknown 生成,如果请求的脚本不存在或其路径中存在路径遍历,如 /var/www/../run/script.php

尽管给定的路径可能存在并在文件系统中解析为绝对路径,但 php-fpm 不进行任何路径解析,并且只接受执行脚本所需的 绝对路径

您可以像这样编程地处理此错误

if (preg_match("#^Primary script unknown\n?$#", $response->getError()))
{
    throw new Exception('Could not find or resolve path to script for execution.');
}

# OR

if ('404 Not Found' === $response->getHeaderLine('Status'))
{
    throw new Exception('Could not find or resolve path to script for execution.');
}

# OR

if ('File not found.' === trim($response->getBody()))
{
    throw new Exception('Could not find or resolve path to script for execution.');
}

准备本地开发环境

这需要在您的机器上安装 dockerdocker-compose

make update

运行示例

make examples

运行所有测试

make tests

命令行工具(仅用于本地调试)

请注意:从版本 v3.1.2 开始,bin/fcgiget 不再包含并链接到 vendor/bin,这是出于安全原因。 (阅读更多。)

通过网络套接字运行调用

docker-compose exec php74 php bin/fcgiget localhost:9001/status

通过 Unix 域套接字运行调用

docker-compose exec php74 php bin/fcgiget unix:///var/run/php-uds.sock/status

这显示了 php-fpm 状态页面的响应。