Skip to content

A PHP fast CGI client for sending requests (a)synchronously to PHP-FPM

License

Notifications You must be signed in to change notification settings

rbro/fast-cgi-client

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

76 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Status Latest Stable Version Total Downloads Coverage Status

Fast CGI Client

A PHP fast CGI client to send requests (a)synchronously to PHP-FPM using the FastCGI Protocol.

This library is based on the work of Pierrick Charron's PHP-FastCGI-Client and was ported and modernized to PHP 7.0/PHP 7.1, extended with some features for handling multiple requests (in loops) and unit and integration tests as well.

You can find an experimental use-case in my related blog posts:

You can also find slides of my talks about this project on speakerdeck.com.


Installation

Use version 1.x for compatibility with PHP 7.0.x

composer require hollodotme/fast-cgi-client:^1.0

PLEASE NOTE: Version 1.4.2 is the last release supporting PHP 7.0.x.

Use version 2.x for compatibility with PHP >= 7.1

composer require hollodotme/fast-cgi-client:^2.0

Usage - single request

The following examples assume a that the content of /path/to/target/script.php looks like this:

<?php declare(strict_types=1);

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

Init client with a unix domain socket connection

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\SocketConnections\UnixDomainSocket;

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

$client = new Client( $connection );

PLEASE NOTE: In versions before 2.3.0 you also need to provide the transport protocol unix:// in the first parameter.

Init client with a network socket connection

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

use hollodotme\FastCGI\Client;
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)
);

$client = new Client( $connection );

Send request synchronously

<?php declare(strict_types=1);

namespace YourVendor\YourProject;

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

$client  = new Client( new UnixDomainSocket( '/var/run/php/php1.0-fpm.sock' ) );
$content = http_build_query(['key' => 'value']);

$request = new PostRequest('/path/to/target/script.php', $content);

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

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

Send request asynchronously (Fire and forget)

<?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( new NetworkSocket( '127.0.0.1', 9000 ) );
$content = http_build_query(['key' => 'value']);

$request = new PostRequest('/path/to/target/script.php', $content);

$requestId = $client->sendAsyncRequest($request);

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

Read the response, after sending the async request

<?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( new NetworkSocket( '127.0.0.1', 9000 ) );
$content = http_build_query(['key' => 'value']);

$request = new PostRequest('/path/to/target/script.php', $content);

$requestId = $client->sendAsyncRequest($request);

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

# Do something else here in the meanwhile

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

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

Notify a callback when async request responded

As of versions 1.2.0 and 2.2.0 you can register response and failure callbacks for each request. In order to notify the callbacks when a response was received instead of returning it, you need to use the waitForResponse(int $requestId, ?int $timeoutMs = null) method.

<?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;

$client  = new Client( 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 paramter
$request->addResponseCallbacks(
	function( ProvidesResponseData $response )
	{
		echo $response->getBody();	
	}
);

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

$requestId = $client->sendAsyncRequest($request);

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

# 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( 
	$requestId,     # The request 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($requestId))
	{
		$client->handleResponse($requestId, 3000);
		break;
	}
}
# prints
value

Usage - multiple requests

Sending multiple requests and reading their responses (order preserved)

<?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( 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']));

$requestIds = [];

$requestIds[] = $client->sendAsyncRequest($request1);
$requestIds[] = $client->sendAsyncRequest($request2);
$requestIds[] = $client->sendAsyncRequest($request3);

echo 'Sent requests with IDs: ' . implode( ', ', $requestIds ) . "\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, ...$requestIds) as $response)
{
	echo $response->getBody() . "\n";	
}
# prints
1
2
3

Sending multiple requests and reading their responses (reactive)

<?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( 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]));

$requestIds = [];

$requestIds[] = $client->sendAsyncRequest($request1);
$requestIds[] = $client->sendAsyncRequest($request2);
$requestIds[] = $client->sendAsyncRequest($request3);

echo 'Sent requests with IDs: ' . implode( ', ', $requestIds ) . "\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() )
{
	$readyRequestIds = $client->getRequestIdsHavingResponse();
	
	# read all ready responses
	foreach ( $client->readResponses( 3000, ...$readyRequestIds ) as $response )
	{
		echo $response->getBody() . "\n";
	}
	
	echo '.';
}

# ... is the same as

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

Sending multiple requests and notifying callbacks (reactive)

<?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;

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

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

$failureCallback = 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);

$requestIds = [];

$requestIds[] = $client->sendAsyncRequest($request1);
$requestIds[] = $client->sendAsyncRequest($request2);
$requestIds[] = $client->sendAsyncRequest($request3);

echo 'Sent requests with IDs: ' . implode( ', ', $requestIds ) . "\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() )
{
	$readyRequestIds = $client->getRequestIdsHavingResponse();
	
	# read all ready responses
	foreach ($readyRequestIds as $requestId)
	{
		$client->handleResponse($requestId, 3000);
	}
}
# prints
3
2
1

Reading output buffer from worker script using pass through callbacks

It may be useful to see the progression of a requested script by having access to the flushed output of that script. The php.ini default output buffering for php-fpm is 4096 bytes and is (hard-coded) disabled for CLI mode. (See documentation) Calling ob_implicit_flush() causes every call to echo or print to immediately be flushed.

The callee script could look like this:

<?php declare(strict_types=1);

ob_implicit_flush();

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

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

echo 'End';

The caller than could look like this:

<?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( new NetworkSocket( '127.0.0.1', 9000 ) );

$passThroughCallback = function( string $buffer )
{
	echo 'Buffer: ' . $buffer;
};

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

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

One
# sleeps 1 sec
Buffer: Two
# sleeps 1 sec
Buffer: Three
# sleeps 1 sec
Buffer: End

Requests

As of version 1.1.0 (PHP 7.0) and 2.1.0 (PHP 7.1), request are defined by the following interface:

<?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;
}

Alongside with this interface, this package provides ab abstract request class, containing default values to make the API more handy for you and 5 request method implementations of this abstract class:

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

So you can either implement the interface, inherit from the abstract class or simply use one of the 5 implementations.

Default values

The abstract request class defines several default values which you can optionally overwrite:

Key Default value Comment
GATEWAY_INTERFACE FastCGI/1.0 Cannot be overwritten, because this is the only supported version of the client.
SERVER_SOFTWARE hollodotme/fast-cgi-client
REMOTE_ADDR 192.168.0.1
REMOTE_PORT 9985
SERVER_ADDR 127.0.0.1
SERVER_PORT 80
SERVER_NAME localhost
SERVER_PROTOCOL HTTP/1.1 You can use the public class constants in hollodotme\FastCGI\Constants\ServerProtocol
CONTENT_TYPE application/x-www-form-urlencoded
REQUEST_URI
CUSTOM_VARS empty array You can use the methods setCustomVar, addCustomVars to add own key-value pairs

Responses

As of version 1.1.0 (PHP 7.0) and 2.1.0 (PHP 7.1), responses are defined by the following interface:

<?php declare(strict_types=1);

namespace hollodotme\FastCGI\Interfaces;

interface ProvidesResponseData
{
	public function getRequestId() : int;

	public function getHeaders() : array;

	public function getHeader( string $headerKey ) : string;

	public function getBody() : string;

	public function getRawResponse() : string;

	public function getDuration() : float;
}

Assuming /path/to/target/script.php has the following content:

<?php declare(strict_types=1);

echo "Hello World";

The raw response would look like this:

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

Hello World

Please note:

  • All headers sent by your script will precede the response body
  • There won't be any HTTP specific headers like HTTP/1.1 200 OK, because there is no webserver involved.

Custom headers will also be part of the response:

<?php declare(strict_types=1);

header('X-Custom: Header');

echo "Hello World";

The raw response would look like this:

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

Hello World

You can retrieve all of the response data separately from the response object:

# Get the request ID
echo $response->getRequestId(); # random int set by client

# Get a single response header
echo $response->getHeader('X-Custom'); # 'Header'

# Get all headers
print_r($response->getHeaders());
/*
Array (
	[X-Custom] => Header
	[Content-type] => text/html; charset=UTF-8
)
*/

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

# Get the raw response
echo $response->getRawResponse();
/*
X-Custom: Header
Content-type: text/html; charset=UTF-8

Hello World
*/

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

Trouble shooting

"File not found." response

If you're facing a File not found. response after issuing a request to PHP-FPM, please make sure the given path to the script you want to call is an absolute path / realpath.

Doesn't work

<?php

$request = new PostRequest( __DIR__ . '/../../run/script.php', $content );
$request = new PostRequest( '/var/www/example.com/../../run/script.php', $content );

Does work

<?php

$request = new PostRequest( dirname(__DIR__, 2). '/run/script.php', $content );
$request = new PostRequest( '/var/run/script.php', $content );

Run examples

php bin/examples.php

Command line tool (for debugging only)

Run a call through a network socket:

bin/fcgiget localhost:9000/status

Run a call through a Unix Domain Socket

bin/fcgiget /var/run/php/php7.1-fpm.sock/status

This shows the response of the php-fpm status page, if enabled.

About

A PHP fast CGI client for sending requests (a)synchronously to PHP-FPM

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • PHP 100.0%