There was some frustration in finding a solution that actually works, so I ended up building a service based on fsockopen() that can handle both GET and POST requests, without being blocking.
Below is the service class:
class NonBlockingHttpClientService {
private $method = 'GET';
private $params = [];
private $port = 80;
private $host;
private $path;
private $post_content;
public function isPost(): bool
{
return ($this->method === 'POST');
}
public function setMethodToPost(): NonBlockingHttpClientService
{
$this->method = 'POST';
return $this;
}
public function setPort(int $port): NonBlockingHttpClientService
{
$this->port = $port;
return $this;
}
public function setParams(array $params): NonBlockingHttpClientService
{
$this->params = $params;
return $this;
}
private function handleUrl(string $url): void
{
$url = str_replace(['https://', 'http://'], '', $url);
$url_parts = explode('/', $url);
if(count($url_parts) < 2) {
$this->host = $url_parts[0];
$this->path = '/';
} else {
$this->host = $url_parts[0];
$this->path = str_replace($this->host, '', $url);
}
}
private function handleParams(): void
{
if(empty($this->params)) return;
if($this->isPost()) {
$this->post_content = http_build_query($this->params);
} else {
/*
if you want to specify the params as an array for GET request, they will just be
appended to the path as a query string
*/
if(strpos($this->path, '?') === false) {
$this->path .= '?' . ltrim($this->arrayToQueryString($this->params), '&');
} else {
$this->path .= $this->arrayToQueryString($this->params);
}
}
}
private function arrayToQueryString(array $params): string
{
$string = '';
foreach($params as $name => $value) {
$string .= "&$name=" . urlencode($value);
}
return $string;
}
public function doRequest(string $url): bool
{
$this->handleUrl($url);
$this->handleParams();
$host = $this->host;
$path = $this->path;
$fp = fsockopen($host, $this->port, $errno, $errstr, 1);
if (!$fp) {
$error_message = __CLASS__ . ": cannot open connection to $host$path : $errstr ($errno)";
echo $error_message;
error_log($error_message);
return false;
} else {
fwrite($fp, $this->method . " $path HTTP/1.1\r\n");
fwrite($fp, "Host: $host\r\n");
if($this->isPost()) fwrite($fp, "Content-Type: application/x-www-form-urlencoded\r\n");
if($this->isPost()) fwrite($fp, "Content-Length: " . strlen($this->post_content) . "\r\n");
fwrite($fp, "Connection: close\r\n");
fwrite($fp, "\r\n");
if($this->isPost()) fwrite($fp, $this->post_content);
return true;
}
}
}
It can be used like this:
$req = new NonBlockingHttpClientService();
$req->setMethodToPost(); //default is GET, so just omit this for GET requests
$req->setParams([
'test2' => 'aaaa', //if parameters are specified both with setParams() and in the query string, for GET requests, params specified with setParams() will take precedence
'test3' => 'bbbb',
'time' => date('H:i:s')
]);
$req->doRequest('test.localhost/some_path/slow_api.php?test1=value1&test2=value2');
And the slow_api.php file, can be something like this.
<?php
error_log('start');
sleep(10);
error_log(print_r($_REQUEST, 1) . 'end');
I find it easier to monitor (tail -f) the error log in order to see what is happening.
pfsockopento open persistent socket connection or fork curl process or if you don't care about delay of actual request (eg. in a log scenario) you can log your request to a log file and have background process like cron send requests out of band using the log entries.