Skip to content

Commit 22f75c4

Browse files
authored
feat(php): Restore Guzzle as a suggestion (#888)
1 parent 02ba561 commit 22f75c4

File tree

4 files changed

+178
-5
lines changed

4 files changed

+178
-5
lines changed

clients/algoliasearch-client-php/lib/Algolia.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,29 @@ public static function setLogger(LoggerInterface $logger)
8686

8787
public static function getHttpClient()
8888
{
89+
$guzzleVersion = null;
90+
if (interface_exists('\GuzzleHttp\ClientInterface')) {
91+
if (defined('\GuzzleHttp\ClientInterface::VERSION')) {
92+
$guzzleVersion = (int) mb_substr(
93+
\GuzzleHttp\Client::VERSION,
94+
0,
95+
1
96+
);
97+
} else {
98+
$guzzleVersion = \GuzzleHttp\ClientInterface::MAJOR_VERSION;
99+
}
100+
}
101+
89102
if (null === self::$httpClient) {
90-
self::setHttpClient(
91-
new \Algolia\AlgoliaSearch\Http\GuzzleHttpClient()
92-
);
103+
if (class_exists('\GuzzleHttp\Client') && 6 <= $guzzleVersion) {
104+
self::setHttpClient(
105+
new \Algolia\AlgoliaSearch\Http\GuzzleHttpClient()
106+
);
107+
} else {
108+
self::setHttpClient(
109+
new \Algolia\AlgoliaSearch\Http\CurlHttpClient()
110+
);
111+
}
93112
}
94113

95114
return self::$httpClient;
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Http;
4+
5+
use Algolia\AlgoliaSearch\Http\Psr7\Response;
6+
use Psr\Http\Message\RequestInterface;
7+
8+
final class CurlHttpClient implements HttpClientInterface
9+
{
10+
private $curlMHandle;
11+
12+
private $curlOptions;
13+
14+
public function __construct($curlOptions = [])
15+
{
16+
$this->curlOptions = $curlOptions;
17+
}
18+
19+
public function sendRequest(
20+
RequestInterface $request,
21+
$timeout,
22+
$connectTimeout
23+
) {
24+
$curlHandle = curl_init();
25+
26+
// set curl options
27+
try {
28+
foreach ($this->curlOptions as $curlOption => $optionValue) {
29+
curl_setopt($curlHandle, constant($curlOption), $optionValue);
30+
}
31+
} catch (\Exception $e) {
32+
$this->invalidOptions($this->curlOptions, $e->getMessage());
33+
}
34+
35+
$curlHeaders = [];
36+
foreach ($request->getHeaders() as $key => $values) {
37+
$curlHeaders[] = $key . ': ' . implode(',', $values);
38+
}
39+
40+
curl_setopt($curlHandle, CURLOPT_HTTPHEADER, $curlHeaders);
41+
42+
curl_setopt(
43+
$curlHandle,
44+
CURLOPT_USERAGENT,
45+
implode(',', $request->getHeader('User-Agent'))
46+
);
47+
//Return the output instead of printing it
48+
curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
49+
curl_setopt($curlHandle, CURLOPT_FAILONERROR, true);
50+
curl_setopt($curlHandle, CURLOPT_ENCODING, '');
51+
curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true);
52+
curl_setopt($curlHandle, CURLOPT_SSL_VERIFYHOST, 2);
53+
// TODO: look into cert
54+
// curl_setopt($curlHandle, CURLOPT_CAINFO, $this->caInfoPath);
55+
56+
curl_setopt($curlHandle, CURLOPT_URL, (string) $request->getUri());
57+
$version = curl_version();
58+
if (
59+
version_compare($version['version'], '7.16.2', '>=') &&
60+
$connectTimeout < 1
61+
) {
62+
curl_setopt(
63+
$curlHandle,
64+
CURLOPT_CONNECTTIMEOUT_MS,
65+
$connectTimeout * 1000
66+
);
67+
curl_setopt($curlHandle, CURLOPT_TIMEOUT_MS, $timeout * 1000);
68+
} else {
69+
curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, $connectTimeout);
70+
curl_setopt($curlHandle, CURLOPT_TIMEOUT, $timeout);
71+
}
72+
73+
// The problem is that on (Li|U)nix, when libcurl uses the standard name resolver,
74+
// a SIGALRM is raised during name resolution which libcurl thinks is the timeout alarm.
75+
curl_setopt($curlHandle, CURLOPT_NOSIGNAL, 1);
76+
curl_setopt($curlHandle, CURLOPT_FAILONERROR, false);
77+
78+
$method = $request->getMethod();
79+
if ('GET' === $method) {
80+
curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'GET');
81+
curl_setopt($curlHandle, CURLOPT_HTTPGET, true);
82+
curl_setopt($curlHandle, CURLOPT_POST, false);
83+
} else {
84+
if ('POST' === $method) {
85+
$body = (string) $request->getBody();
86+
curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
87+
curl_setopt($curlHandle, CURLOPT_POST, true);
88+
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body);
89+
} elseif ('DELETE' === $method) {
90+
curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE');
91+
curl_setopt($curlHandle, CURLOPT_POST, false);
92+
} elseif ('PUT' === $method) {
93+
$body = (string) $request->getBody();
94+
curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT');
95+
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body);
96+
curl_setopt($curlHandle, CURLOPT_POST, true);
97+
}
98+
}
99+
$mhandle = $this->getMHandle($curlHandle);
100+
101+
// Do all the processing.
102+
$running = null;
103+
do {
104+
$mrc = curl_multi_exec($mhandle, $running);
105+
} while (CURLM_CALL_MULTI_PERFORM === $mrc);
106+
107+
while ($running && CURLM_OK === $mrc) {
108+
if (-1 === curl_multi_select($mhandle, 0.1)) {
109+
usleep(100);
110+
}
111+
do {
112+
$mrc = curl_multi_exec($mhandle, $running);
113+
} while (CURLM_CALL_MULTI_PERFORM === $mrc);
114+
}
115+
116+
$statusCode = (int) curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
117+
$responseBody = curl_multi_getcontent($curlHandle);
118+
$error = curl_error($curlHandle);
119+
120+
$this->releaseMHandle($curlHandle);
121+
curl_close($curlHandle);
122+
123+
return new Response($statusCode, [], $responseBody, '1.1', $error);
124+
}
125+
126+
private function getMHandle($curlHandle)
127+
{
128+
if (!is_resource($this->curlMHandle)) {
129+
$this->curlMHandle = curl_multi_init();
130+
}
131+
curl_multi_add_handle($this->curlMHandle, $curlHandle);
132+
133+
return $this->curlMHandle;
134+
}
135+
136+
private function releaseMHandle($curlHandle)
137+
{
138+
curl_multi_remove_handle($this->curlMHandle, $curlHandle);
139+
}
140+
141+
private function invalidOptions(array $curlOptions = [], $errorMsg = '')
142+
{
143+
throw new \OutOfBoundsException(
144+
sprintf(
145+
'AlgoliaSearch curloptions options keys are invalid. %s given. error message : %s',
146+
json_encode($curlOptions),
147+
$errorMsg
148+
)
149+
);
150+
}
151+
}

config/generation.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ module.exports = {
2828
'!clients/algoliasearch-client-javascript/packages/algoliasearch/jest.config.ts',
2929

3030
// PHP
31-
'!clients/algoliasearch-client-php/*',
31+
'!clients/algoliasearch-client-php/**',
3232
'clients/algoliasearch-client-php/lib/Api/*',
3333
'clients/algoliasearch-client-php/lib/Model/**',
3434
'clients/algoliasearch-client-php/lib/Configuration/*',
3535
'clients/algoliasearch-client-php/lib/ApiException.php',
3636
'clients/algoliasearch-client-php/lib/ObjectSerializer.php',
37+
'clients/algoliasearch-client-php/composer.json',
3738
],
3839
};

templates/php/composer.mustache

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
"ext-curl": "*",
2929
"ext-json": "*",
3030
"ext-mbstring": "*",
31-
"guzzlehttp/guzzle": "^7.3",
3231
"guzzlehttp/psr7": "^2.0",
3332
"psr/http-message": "^1.0",
3433
"psr/log": "^1.0 || ^2.0 || ^3.0",
@@ -47,5 +46,8 @@
4746
},
4847
"autoload-dev": {
4948
"psr-4": { "{{escapedInvokerPackage}}\\Test\\" : "{{testBasePath}}/" }
49+
},
50+
"suggest": {
51+
"guzzlehttp/guzzle": "If you prefer to use Guzzle HTTP client instead of the Http Client implementation provided by the package"
5052
}
5153
}

0 commit comments

Comments
 (0)